Skip to main content

uv_cli/
settings.rs

1//! Resolve command settings from CLI arguments, environment variables, and configuration files.
2
3use std::env::VarError;
4use std::ffi::OsString;
5use std::fmt;
6use std::num::NonZeroUsize;
7use std::path::{Path, PathBuf};
8use std::process;
9use std::str::FromStr;
10use std::time::Duration;
11
12use anyhow::{Result, bail};
13use rustc_hash::FxHashSet;
14
15use uv_audit::{VulnerabilityID, VulnerabilityServiceFormat};
16use uv_auth::Service;
17use uv_cache::{CacheArgs, Refresh};
18use uv_client::{Certificates, Connectivity, MetadataRangeRequest};
19use uv_configuration::{
20    ActiveEnvironment, AddBoundsKind, AnnotationStyle, BuildIsolation, BuildOptions, Concurrency,
21    DependencyGroups, DependencyMode, DevMode, DryRun, EditableMode, EnvFile, ExcludeDependency,
22    ExcludeNewer, ExcludeNewerPackage, ExportFormat, ExtrasSpecification, ForkStrategy,
23    GitLfsSetting, HashCheckingMode, IndexStrategy, InitKind, InitProjectKind, InstallOptions,
24    KeyringProviderType, Modifications, NoBinary, NoBuild, NoSources, Override, PackageOverride,
25    PipCompileFormat, Prerelease, ProjectBuildBackend, ProxyUrl, PythonUpgrade,
26    PythonUpgradeSource, Reinstall, RequiredVersion, RequirementsInput, ResolutionMode,
27    TargetTriple, ToolRunCommand, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem,
28};
29use uv_distribution_types::{
30    ConfigSettings, DependencyMetadata, ExcludeNewerOverride, ExtraBuildVariables, Index,
31    IndexLocations, IndexUrl, MinimumLibcVersion, NameRequirementSpecification,
32    PackageConfigSettings, Requirement,
33};
34use uv_install_wheel::LinkMode;
35use uv_normalize::{ExtraName, PackageName, PipGroupName};
36use uv_pep440::Version;
37use uv_pep508::{MarkerTree, RequirementOrigin};
38use uv_preview::Preview;
39use uv_pypi_types::SupportedEnvironments;
40use uv_python_types::{
41    Prefix, PythonArchitecture, PythonDownloads, PythonPreference, PythonVersion, Target,
42};
43use uv_redacted::SafeUrl;
44use uv_settings::{
45    Combine, EnvFlag, EnvironmentOptions, FilesystemOptions, FrozenFlag, FrozenSource,
46    IndexOptions, LockCheck, LockedFlag, LockedSource, MalwareCheckSettings, Options, PipOptions,
47    PreviewFeaturesOption, PreviewOption, PublishOptions, PythonInstallMirrors, PythonListKinds,
48    ResolverInstallerOptions, ResolverInstallerSchema, ResolverInstallerSettings, ResolverOptions,
49    ResolverSettings, resolve_build_hash_checking, resolve_prerelease,
50};
51use uv_static::EnvVars;
52use uv_torch::{AmdGpuArchitecture, TorchMode};
53use uv_warnings::warn_user_once;
54use uv_workspace::pyproject::{DependencyType, ExtraBuildDependencies, OverrideDependency};
55
56use crate::comma::CommaSeparatedRequirements;
57use crate::{
58    AddArgs, AuditArgs, AuditCommonArgs, AuditOutputFormat, AuthLoginArgs, AuthLogoutArgs,
59    AuthTokenArgs, ColorChoice, DependencyConstraintsArgs, ExternalCommand, GlobalArgs, InitArgs,
60    ListFormat, LockArgs, Maybe, MetadataArgs, PipCheckArgs, PipCompileArgs, PipFreezeArgs,
61    PipInstallArgs, PipInstallFormat, PipListArgs, PipShowArgs, PipSyncArgs, PipTreeArgs,
62    PipUninstallArgs, ProjectDependencyGroupsArgs, PythonFindArgs, PythonInstallArgs,
63    PythonListArgs, PythonListFormat, PythonPinArgs, PythonUninstallArgs, PythonUpgradeArgs,
64    RemoveArgs, RunArgs, SyncArgs, SyncFormat, ToolAuditArgs, ToolDirArgs, ToolInstallArgs,
65    ToolListArgs, ToolRunArgs, ToolUninstallArgs, TreeArgs, TreeFormat, UpgradeArgs, VenvArgs,
66    VersionArgs, VersionBumpSpec, VersionFormat,
67};
68use crate::{
69    AuthorFrom, BuildArgs, BuildOptionsArgs, CheckArgs, ExcludeNewerArgs, ExportArgs, FormatArgs,
70    HashCheckingArgs, PackageExcludeNewerArgs, PublishArgs, PythonDirArgs, RegistryClientArgs,
71    ResolverArgs, ResolverInstallerArgs, ToolUpgradeArgs,
72    options::{
73        Flag, FlagSource, IntoPipOptions, check_conflicts, flag, resolve_flag, resolve_flag_pair,
74        resolver_installer_options, resolver_options, upgrade_options,
75    },
76};
77
78/// The default publish URL.
79const PYPI_PUBLISH_URL: &str = "https://upload.pypi.org/legacy/";
80
81/// The resolved global settings to use for any invocation of the CLI.
82#[derive(Debug, Clone)]
83pub struct GlobalSettings {
84    pub required_version: Option<RequiredVersion>,
85    pub quiet: u8,
86    pub verbose: u8,
87    pub color: ColorChoice,
88    pub network_settings: NetworkSettings,
89    pub concurrency: Concurrency,
90    pub show_settings: bool,
91    pub preview: Preview,
92    pub python_preference: PythonPreference,
93    pub python_arch: Option<PythonArchitecture>,
94    pub python_downloads: PythonDownloads,
95    pub no_progress: bool,
96    pub installer_metadata: bool,
97}
98
99impl GlobalSettings {
100    /// Resolve the [`GlobalSettings`] from the CLI and filesystem configuration.
101    pub fn resolve(
102        args: &GlobalArgs,
103        workspace: Option<&FilesystemOptions>,
104        environment: &EnvironmentOptions,
105        custom_certificate_file: Option<&Path>,
106    ) -> anyhow::Result<Self> {
107        let network_settings =
108            NetworkSettings::resolve(args, workspace, environment, custom_certificate_file)?;
109        let python_preference = resolve_python_preference(args, workspace, environment)?;
110        let color = resolve_color(args);
111        Ok(Self {
112            required_version: workspace
113                .and_then(|workspace| workspace.globals.required_version.clone()),
114            quiet: args.quiet,
115            verbose: args.verbose,
116            color,
117            network_settings,
118            concurrency: Concurrency::new(
119                environment
120                    .concurrency
121                    .downloads
122                    .combine(workspace.and_then(|workspace| workspace.globals.concurrent_downloads))
123                    .map(NonZeroUsize::get)
124                    .unwrap_or(Concurrency::DEFAULT_DOWNLOADS),
125                environment
126                    .concurrency
127                    .builds
128                    .combine(workspace.and_then(|workspace| workspace.globals.concurrent_builds))
129                    .map(NonZeroUsize::get)
130                    .unwrap_or_else(Concurrency::threads),
131                environment
132                    .concurrency
133                    .installs
134                    .combine(workspace.and_then(|workspace| workspace.globals.concurrent_installs))
135                    .map(NonZeroUsize::get)
136                    .unwrap_or_else(Concurrency::threads),
137                environment
138                    .concurrency
139                    .cache_reads
140                    .map(NonZeroUsize::get)
141                    .unwrap_or(Concurrency::DEFAULT_CACHE_READS),
142            ),
143            show_settings: args.show_settings,
144            preview: resolve_preview(args, workspace, environment)?,
145            python_preference,
146            python_arch: environment.python_arch,
147            python_downloads: flag(
148                args.allow_python_downloads,
149                args.no_python_downloads,
150                "python-downloads",
151            )?
152            .map(PythonDownloads::from)
153            .combine(env(env::UV_PYTHON_DOWNLOADS))
154            .combine(workspace.and_then(|workspace| workspace.globals.python_downloads))
155            .unwrap_or_default(),
156            // Disable the progress bar with `RUST_LOG` to avoid progress fragments interleaving
157            // with log messages.
158            no_progress: resolve_flag(args.no_progress, "no-progress", environment.no_progress)
159                .is_enabled()
160                || std::env::var_os(EnvVars::RUST_LOG).is_some(),
161            installer_metadata: !resolve_flag(
162                args.no_installer_metadata,
163                "no-installer-metadata",
164                environment.no_installer_metadata,
165            )
166            .is_enabled(),
167        })
168    }
169}
170
171/// Resolve the color choice from CLI arguments and environment variables.
172pub fn resolve_color(args: &GlobalArgs) -> ColorChoice {
173    if let Some(color_choice) = args.color {
174        // If `--color` is passed explicitly, use its value.
175        color_choice
176    } else if args.no_color {
177        // If `--no-color` is passed explicitly, disable color output.
178        ColorChoice::Never
179    } else if std::env::var_os(EnvVars::NO_COLOR)
180        .as_ref()
181        .is_some_and(|v| !v.is_empty())
182    {
183        // If the `NO_COLOR` is set, disable color output.
184        ColorChoice::Never
185    } else if std::env::var_os(EnvVars::FORCE_COLOR)
186        .as_ref()
187        .is_some_and(|v| !v.is_empty())
188        || std::env::var_os(EnvVars::CLICOLOR_FORCE)
189            .as_ref()
190            .is_some_and(|v| !v.is_empty())
191    {
192        // If `FORCE_COLOR` or `CLICOLOR_FORCE` is set, always enable color output.
193        ColorChoice::Always
194    } else {
195        ColorChoice::Auto
196    }
197}
198
199fn resolve_python_preference(
200    args: &GlobalArgs,
201    workspace: Option<&FilesystemOptions>,
202    environment: &EnvironmentOptions,
203) -> anyhow::Result<PythonPreference> {
204    // Resolve flags from CLI and environment variables.
205    let (managed_python, no_managed_python) = resolve_flag_pair(
206        args.managed_python,
207        args.no_managed_python,
208        "managed-python",
209        "no-managed-python",
210        Some(environment.managed_python),
211        Some(environment.no_managed_python),
212    );
213
214    // Check for conflicts between managed_python and python_preference.
215    if managed_python.is_enabled() && args.python_preference.is_some() {
216        check_conflicts(managed_python, Flag::from_cli("python-preference"))?;
217    }
218
219    // Check for conflicts between no_managed_python and python_preference.
220    if no_managed_python.is_enabled() && args.python_preference.is_some() {
221        check_conflicts(no_managed_python, Flag::from_cli("python-preference"))?;
222    }
223
224    Ok(if managed_python.is_enabled() {
225        PythonPreference::OnlyManaged
226    } else if no_managed_python.is_enabled() {
227        PythonPreference::OnlySystem
228    } else {
229        args.python_preference
230            .combine(workspace.and_then(|workspace| workspace.globals.python_preference))
231            .unwrap_or_default()
232    })
233}
234
235/// Resolve the preview setting from CLI, environment, and workspace config.
236pub fn resolve_preview(
237    args: &GlobalArgs,
238    workspace: Option<&FilesystemOptions>,
239    environment: &EnvironmentOptions,
240) -> anyhow::Result<Preview> {
241    // Explicit `--preview` and `--no-preview` flags take priority.
242    if let Some(enabled) = flag(args.preview, args.no_preview, "preview")? {
243        return Ok(if enabled {
244            Preview::all()
245        } else {
246            Preview::default()
247        });
248    }
249
250    // `UV_PREVIEW=true` enables all preview features.
251    if environment.preview.value == Some(true) {
252        return Ok(Preview::all());
253    }
254
255    let configured = workspace.and_then(|workspace| workspace.globals.preview.as_ref());
256
257    // Boolean enable-all configuration takes priority.
258    if matches!(
259        configured,
260        Some(
261            PreviewOption::Preview(true)
262                | PreviewOption::PreviewFeatures(PreviewFeaturesOption::Toggle(true))
263        )
264    ) {
265        return Ok(Preview::all());
266    }
267
268    // Explicit preview feature names take priority over configured feature names.
269    if !args.preview_features.is_empty() {
270        return Ok(Preview::from_feature_names(&args.preview_features));
271    }
272
273    // Fall back to workspace configuration.
274    Ok(configured.map(PreviewOption::resolve).unwrap_or_default())
275}
276
277/// The resolved network settings to use for any invocation of the CLI.
278#[derive(Debug, Clone)]
279pub struct NetworkSettings {
280    pub connectivity: Connectivity,
281    offline: Flag,
282    pub system_certs: bool,
283    pub custom_certificates: Option<Certificates>,
284    pub http_proxy: Option<ProxyUrl>,
285    pub https_proxy: Option<ProxyUrl>,
286    pub no_proxy: Option<Vec<String>>,
287    pub allow_insecure_host: Vec<TrustedHost>,
288    pub read_timeout: Duration,
289    pub connect_timeout: Duration,
290    pub retries: u32,
291    pub metadata_range_request: MetadataRangeRequest,
292}
293
294impl NetworkSettings {
295    #[allow(deprecated)]
296    fn resolve(
297        args: &GlobalArgs,
298        workspace: Option<&FilesystemOptions>,
299        environment: &EnvironmentOptions,
300        custom_certificate_file: Option<&Path>,
301    ) -> anyhow::Result<Self> {
302        // Resolve offline flag from CLI, environment variable, and workspace config.
303        // Precedence: CLI > Env var > Workspace config > default (false).
304        let offline = match flag(args.offline, args.no_offline, "offline")? {
305            Some(true) => Flag::from_cli("offline"),
306            Some(false) => Flag::disabled(),
307            None => {
308                // CLI didn't provide a value, check environment variable.
309                let env_flag = resolve_flag(false, "offline", environment.offline);
310                if environment.offline.value.is_some() {
311                    env_flag
312                } else if workspace
313                    .and_then(|workspace| workspace.globals.offline)
314                    .unwrap_or(false)
315                {
316                    // Workspace config enabled offline mode.
317                    Flag::from_config("offline")
318                } else {
319                    Flag::disabled()
320                }
321            }
322        };
323
324        let connectivity = if offline.is_enabled() {
325            Connectivity::Offline
326        } else {
327            Connectivity::Online
328        };
329
330        if args.native_tls {
331            warn_user_once!(
332                "The `--native-tls` flag is deprecated and will be removed in a future release. Use `--system-certs` instead."
333            );
334        }
335        if args.no_native_tls {
336            warn_user_once!(
337                "The `--no-native-tls` flag is deprecated and will be removed in a future release. Use `--no-system-certs` instead."
338            );
339        }
340        if environment.native_tls.value.is_some() && environment.system_certs.value.is_none() {
341            warn_user_once!(
342                "The `UV_NATIVE_TLS` environment variable is deprecated and will be removed in a future release. Use `UV_SYSTEM_CERTS` instead."
343            );
344        }
345        if let Some(workspace) = workspace
346            && workspace.globals.native_tls.is_some()
347            && workspace.globals.system_certs.is_none()
348        {
349            warn_user_once!(
350                "The `native-tls` setting is deprecated and will be removed in a future release. Use `system-certs` instead."
351            );
352        }
353
354        // Resolve whether to use system certificates.
355        //
356        // `--native-tls` is a legacy alias for `--system-certs` — it enables system certificates
357        // but does NOT change the TLS backend. Any explicit CLI setting should take precedence
358        // over environment variables and workspace configuration, regardless of which spelling is
359        // used.
360        let system_certs =
361            if let Some(value) = flag(args.system_certs, args.no_system_certs, "system-certs")? {
362                value
363            } else if let Some(value) = flag(args.native_tls, args.no_native_tls, "native-tls")? {
364                value
365            } else if let Some(value) = environment.system_certs.value {
366                value
367            } else if let Some(value) = environment.native_tls.value {
368                value
369            } else {
370                workspace
371                    .and_then(|workspace| {
372                        workspace
373                            .globals
374                            .system_certs
375                            .or(workspace.globals.native_tls)
376                    })
377                    .unwrap_or(false)
378            };
379
380        let allow_insecure_host = args
381            .allow_insecure_host
382            .as_ref()
383            .map(|allow_insecure_host| {
384                allow_insecure_host
385                    .iter()
386                    .filter_map(|value| value.clone().into_option())
387            })
388            .into_iter()
389            .flatten()
390            .chain(
391                workspace
392                    .and_then(|workspace| workspace.globals.allow_insecure_host.clone())
393                    .into_iter()
394                    .flatten(),
395            )
396            .collect();
397        let http_proxy = workspace.and_then(|workspace| workspace.globals.http_proxy.clone());
398        let https_proxy = workspace.and_then(|workspace| workspace.globals.https_proxy.clone());
399        let no_proxy = workspace.and_then(|workspace| workspace.globals.no_proxy.clone());
400
401        let custom_certificates = custom_certificate_file
402            .map(Certificates::from_file)
403            .transpose()?
404            .or_else(Certificates::from_env);
405
406        Ok(Self {
407            connectivity,
408            offline,
409            system_certs,
410            custom_certificates,
411            http_proxy,
412            https_proxy,
413            no_proxy,
414            allow_insecure_host,
415            read_timeout: environment.http_read_timeout,
416            connect_timeout: environment.http_connect_timeout,
417            retries: environment.http_retries,
418            metadata_range_request: environment
419                .require_metadata_range_requests
420                .unwrap_or_default()
421                .into(),
422        })
423    }
424
425    /// Check if offline mode conflicts with a refresh request.
426    ///
427    /// This should be called when a command uses refresh functionality to ensure
428    /// offline mode and refresh are not both enabled.
429    pub fn check_refresh_conflict(&self, refresh: &Refresh) -> anyhow::Result<()> {
430        if !matches!(refresh, Refresh::None(_)) {
431            // TODO(charlie): `Refresh` isn't a `Flag`, so we create a synthetic one here
432            // (which matches Clap's representation). Consider a dedicated helper for
433            // conflicts with CLI-only arguments.
434            check_conflicts(self.offline, Flag::from_cli("refresh"))?;
435        }
436        Ok(())
437    }
438}
439
440/// The resolved cache settings to use for any invocation of the CLI.
441#[derive(Debug, Clone)]
442pub struct CacheSettings {
443    pub no_cache: bool,
444    pub cache_dir: Option<PathBuf>,
445}
446
447impl CacheSettings {
448    /// Resolve the [`CacheSettings`] from the CLI, environment, and filesystem configuration.
449    pub fn resolve(
450        args: CacheArgs,
451        workspace: Option<&FilesystemOptions>,
452        environment: &EnvironmentOptions,
453    ) -> Self {
454        Self {
455            no_cache: args
456                .no_cache
457                .then_some(true)
458                .combine(environment.no_cache.value)
459                .combine(workspace.and_then(|workspace| workspace.globals.no_cache))
460                .unwrap_or(false),
461            cache_dir: args
462                .cache_dir
463                .or_else(|| workspace.and_then(|workspace| workspace.globals.cache_dir.clone())),
464        }
465    }
466}
467
468/// The resolved settings to use for a `init` invocation.
469#[derive(Debug, Clone)]
470pub struct InitSettings {
471    pub path: Option<PathBuf>,
472    pub name: Option<PackageName>,
473    pub kind: InitKind,
474    pub bare: bool,
475    pub description: Option<String>,
476    pub no_description: bool,
477    pub vcs: Option<VersionControlSystem>,
478    pub build_backend: Option<ProjectBuildBackend>,
479    pub no_readme: bool,
480    pub author_from: Option<AuthorFrom>,
481    pub pin_python: bool,
482    pub no_workspace: bool,
483    pub python: Option<String>,
484    pub install_mirrors: PythonInstallMirrors,
485}
486
487impl InitSettings {
488    /// Resolve the [`InitSettings`] from the CLI and filesystem configuration.
489    pub fn resolve(
490        args: InitArgs,
491        filesystem: Option<FilesystemOptions>,
492        environment: EnvironmentOptions,
493    ) -> Result<Self> {
494        let InitArgs {
495            path,
496            name,
497            r#virtual,
498            package,
499            no_package,
500            bare,
501            app,
502            lib,
503            script,
504            description,
505            no_description,
506            vcs,
507            build_backend,
508            no_readme,
509            author_from,
510            no_pin_python,
511            pin_python,
512            no_workspace,
513            python,
514            ..
515        } = args;
516
517        let bare = resolve_flag(bare, "bare", environment.init_bare).is_enabled();
518
519        let filesystem_install_mirrors = filesystem
520            .map(|fs| fs.install_mirrors.clone())
521            .unwrap_or_default();
522
523        let no_description = no_description || (bare && description.is_none());
524
525        if r#virtual && lib {
526            bail!("`--virtual` and `--lib` are mutually exclusive");
527        }
528        if r#virtual && build_backend.is_some() {
529            bail!("`--virtual` and `--build-backend` are mutually exclusive");
530        }
531
532        let package = flag(
533            package || build_backend.is_some(),
534            no_package || r#virtual,
535            "virtual",
536        )?;
537
538        let kind = if script {
539            InitKind::Script
540        } else if bare {
541            if package == Some(true) || lib {
542                InitKind::Project(InitProjectKind::BareWithBuildSystem)
543            } else {
544                InitKind::Project(InitProjectKind::Bare)
545            }
546        } else {
547            // Merge `--app` and `--lib`.
548            let app_lib_kind = match (app, lib) {
549                (false, false) => InitProjectKind::ApplicationWithLibrary,
550                (true, false) => InitProjectKind::Application,
551                (false, true) => InitProjectKind::Library,
552                (true, true) => bail!("`app` and `lib` are mutually exclusive"),
553            };
554
555            // Apply overrides from `--package`/`--no-package`.
556            let app_lib_kind = match (app_lib_kind, package) {
557                (InitProjectKind::ApplicationWithLibrary, None | Some(true)) => {
558                    InitProjectKind::ApplicationWithLibrary
559                }
560                (InitProjectKind::ApplicationWithLibrary, Some(false)) => {
561                    InitProjectKind::Application
562                }
563                (InitProjectKind::Application, None | Some(true)) => {
564                    InitProjectKind::ApplicationWithLibrary
565                }
566                (InitProjectKind::Application, Some(false)) => InitProjectKind::Application,
567                (InitProjectKind::Library, None | Some(true)) => InitProjectKind::Library,
568                (InitProjectKind::Library, Some(false)) => {
569                    bail!("`lib` and `no_package` are mutually exclusive");
570                }
571                (InitProjectKind::Bare | InitProjectKind::BareWithBuildSystem, _) => {
572                    unreachable!()
573                }
574            };
575            InitKind::Project(app_lib_kind)
576        };
577
578        if script && package == Some(true) {
579            warn_user_once!("`--package` is a no-op for Python scripts, which are standalone");
580        }
581
582        Ok(Self {
583            path,
584            name,
585            kind,
586            bare,
587            description,
588            no_description,
589            vcs: vcs.or(bare.then_some(VersionControlSystem::None)),
590            build_backend,
591            no_readme,
592            author_from,
593            pin_python: flag(pin_python, no_pin_python, "pin-python")?.unwrap_or(!bare),
594            no_workspace,
595            python: python.and_then(Maybe::into_option),
596            install_mirrors: environment
597                .install_mirrors
598                .combine(filesystem_install_mirrors),
599        })
600    }
601}
602
603impl From<LockCheck> for Flag {
604    fn from(lock_check: LockCheck) -> Self {
605        match lock_check {
606            LockCheck::Enabled(LockedSource::Cli(flag)) => Self::from_cli(flag.name()),
607            LockCheck::Enabled(LockedSource::Env) => Self::Enabled {
608                source: FlagSource::Env(EnvVars::UV_LOCKED),
609                name: "locked",
610            },
611            LockCheck::Disabled => Self::disabled(),
612        }
613    }
614}
615
616impl From<FrozenSource> for Flag {
617    fn from(source: FrozenSource) -> Self {
618        match source {
619            FrozenSource::Cli(flag) => Self::from_cli(flag.name()),
620            FrozenSource::Env => Self::Enabled {
621                source: FlagSource::Env(EnvVars::UV_FROZEN),
622                name: "frozen",
623            },
624        }
625    }
626}
627
628/// Resolve conflicting lock flags, letting CLI arguments override environment variables.
629fn resolve_lock_flags(
630    locked: LockCheck,
631    frozen: Option<FrozenSource>,
632) -> anyhow::Result<(LockCheck, Option<FrozenSource>)> {
633    match (locked, frozen) {
634        (LockCheck::Enabled(LockedSource::Cli(flag)), Some(FrozenSource::Env)) => {
635            warn_user_once!("Ignoring `UV_FROZEN` because `{flag}` was provided");
636            Ok((locked, None))
637        }
638        (LockCheck::Enabled(LockedSource::Env), Some(FrozenSource::Cli(flag))) => {
639            warn_user_once!("Ignoring `UV_LOCKED` because `{flag}` was provided");
640            Ok((LockCheck::Disabled, frozen))
641        }
642        _ => {
643            check_conflicts(
644                Flag::from(locked),
645                frozen.map_or(Flag::Disabled, Flag::from),
646            )?;
647            Ok((locked, frozen))
648        }
649    }
650}
651
652/// Resolve frozen mode and its source from CLI arguments and the environment.
653fn resolve_frozen(
654    enabled: bool,
655    disabled: bool,
656    cli_flag: FrozenFlag,
657    environment: EnvFlag,
658) -> Option<FrozenSource> {
659    if enabled {
660        Some(FrozenSource::Cli(cli_flag))
661    } else if !disabled && environment.value == Some(true) {
662        Some(FrozenSource::Env)
663    } else {
664        None
665    }
666}
667
668/// Resolve a lock check and its source from CLI arguments and the environment.
669fn resolve_lock_check(
670    enabled: bool,
671    disabled: bool,
672    cli_flag: LockedFlag,
673    environment: EnvFlag,
674) -> LockCheck {
675    if enabled {
676        LockCheck::Enabled(LockedSource::Cli(cli_flag))
677    } else if !disabled && environment.value == Some(true) {
678        LockCheck::Enabled(LockedSource::Env)
679    } else {
680        LockCheck::Disabled
681    }
682}
683
684/// The resolved settings to use for a `run` invocation.
685#[derive(Debug, Clone)]
686pub struct RunSettings {
687    pub lock_check: LockCheck,
688    pub frozen: Option<FrozenSource>,
689    pub extras: ExtrasSpecification,
690    pub groups: DependencyGroups,
691    pub editable: Option<EditableMode>,
692    pub modifications: Modifications,
693    pub with: Vec<String>,
694    pub with_editable: Vec<String>,
695    pub with_requirements: Vec<RequirementsInput>,
696    pub isolated: bool,
697    pub show_resolution: bool,
698    pub all_packages: bool,
699    pub package: Option<PackageName>,
700    pub no_project: bool,
701    pub active: ActiveEnvironment,
702    pub no_sync: bool,
703    pub python: Option<String>,
704    pub python_platform: Option<TargetTriple>,
705    pub install_mirrors: PythonInstallMirrors,
706    pub refresh: Refresh,
707    pub settings: ResolverInstallerSettings,
708    pub env_file: EnvFile,
709    pub max_recursion_depth: u32,
710    pub malware_settings: MalwareCheckSettings,
711    #[cfg(unix)]
712    pub run_rlimit_nofile: Option<u32>,
713}
714
715impl RunSettings {
716    // Default value for UV_RUN_MAX_RECURSION_DEPTH if unset. This is large
717    // enough that it's unlikely a user actually needs this recursion depth,
718    // but short enough that we detect recursion quickly enough to avoid OOMing
719    // or hanging for a long time.
720    const DEFAULT_MAX_RECURSION_DEPTH: u32 = 100;
721
722    /// Resolve the [`RunSettings`] from the CLI and filesystem configuration.
723    pub fn resolve(
724        args: RunArgs,
725        filesystem: Option<FilesystemOptions>,
726        environment: EnvironmentOptions,
727    ) -> anyhow::Result<Self> {
728        let RunArgs {
729            extra,
730            all_extras,
731            no_extra,
732            no_all_extras,
733            dependency_groups:
734                ProjectDependencyGroupsArgs {
735                    dev,
736                    no_dev,
737                    only_dev,
738                    group,
739                    no_group,
740                    no_default_groups,
741                    only_group,
742                    all_groups,
743                },
744            module: _,
745            editable,
746            no_editable,
747            no_editable_package,
748            inexact,
749            exact,
750            script: _,
751            gui_script: _,
752            command: _,
753            with,
754            with_editable,
755            with_requirements,
756            isolated,
757            active,
758            no_active,
759            no_sync,
760            locked,
761            no_locked,
762            frozen,
763            no_frozen,
764            installer,
765            build,
766            refresh,
767            all_packages,
768            package,
769            no_project,
770            python,
771            python_platform,
772            show_resolution,
773            env_file,
774            no_env_file,
775            max_recursion_depth,
776        } = args;
777
778        let filesystem_install_mirrors = filesystem
779            .as_ref()
780            .map(|fs| fs.install_mirrors.clone())
781            .unwrap_or_default();
782
783        // Resolve flags from CLI and environment variables.
784        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
785        let frozen = resolve_frozen(frozen, no_frozen, FrozenFlag::Frozen, environment.frozen);
786        let no_sync = resolve_flag(no_sync, "no-sync", environment.no_sync);
787
788        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
789
790        let (dev, no_dev) = resolve_flag_pair(
791            dev,
792            no_dev,
793            "dev",
794            "no-dev",
795            Some(environment.dev),
796            Some(environment.no_dev),
797        );
798
799        let (editable, no_editable) = resolve_flag_pair(
800            editable,
801            no_editable,
802            "editable",
803            "no-editable",
804            None,
805            Some(environment.no_editable),
806        );
807        let isolated = isolated || environment.isolated.value == Some(true);
808        let show_resolution = show_resolution || environment.show_resolution.value == Some(true);
809        let no_env_file = no_env_file || environment.no_env_file.value == Some(true);
810
811        let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment);
812
813        Ok(Self {
814            lock_check: locked,
815            frozen,
816            extras: ExtrasSpecification::from_args(
817                extra.unwrap_or_default(),
818                no_extra,
819                // TODO(blueraft): support no_default_extras
820                false,
821                // TODO(blueraft): support only_extra
822                vec![],
823                flag(all_extras, no_all_extras, "all-extras")?.unwrap_or_default(),
824            ),
825            groups: DependencyGroups::from_args(
826                DevMode::from_args(dev.into(), no_dev.into(), only_dev),
827                group,
828                if no_group.is_empty() {
829                    environment.no_group.clone().unwrap_or_default()
830                } else {
831                    no_group
832                },
833                no_default_groups,
834                only_group,
835                all_groups,
836            ),
837            editable: EditableMode::from_args(
838                flag(editable.into(), no_editable.into(), "editable")?,
839                no_editable_package,
840            ),
841            modifications: if flag(exact, inexact, "inexact")?.unwrap_or(false) {
842                Modifications::Exact
843            } else {
844                Modifications::Sufficient
845            },
846            with: with
847                .into_iter()
848                .flat_map(CommaSeparatedRequirements::into_iter)
849                .collect(),
850            with_editable: with_editable
851                .into_iter()
852                .flat_map(CommaSeparatedRequirements::into_iter)
853                .collect(),
854            with_requirements: with_requirements
855                .into_iter()
856                .filter_map(Maybe::into_option)
857                .collect(),
858            isolated,
859            show_resolution,
860            all_packages,
861            package,
862            no_project,
863            no_sync: no_sync.is_enabled(),
864            active: flag(active, no_active, "active")?.into(),
865            python: python.and_then(Maybe::into_option),
866            python_platform,
867            refresh: Refresh::try_from(refresh)?,
868            settings: resolve_resolver_installer_settings(
869                installer,
870                build,
871                filesystem,
872                &environment,
873            )?,
874            env_file: EnvFile::from_args(env_file, no_env_file),
875            install_mirrors: environment
876                .install_mirrors
877                .combine(filesystem_install_mirrors),
878            max_recursion_depth: max_recursion_depth.unwrap_or(Self::DEFAULT_MAX_RECURSION_DEPTH),
879            malware_settings,
880            #[cfg(unix)]
881            run_rlimit_nofile: environment.run_rlimit_nofile,
882        })
883    }
884}
885
886/// The resolved settings to use for a `tool run` invocation.
887#[derive(Debug, Clone)]
888pub struct ToolRunSettings {
889    pub command: Option<Vec<OsString>>,
890    pub from: Option<String>,
891    pub with: Vec<String>,
892    pub with_requirements: Vec<RequirementsInput>,
893    pub with_editable: Vec<String>,
894    pub constraints: Vec<RequirementsInput>,
895    pub overrides: Vec<RequirementsInput>,
896    pub build_constraints: Vec<RequirementsInput>,
897    pub isolated: bool,
898    pub show_resolution: bool,
899    pub lfs: GitLfsSetting,
900    pub python: Option<String>,
901    pub python_platform: Option<TargetTriple>,
902    pub install_mirrors: PythonInstallMirrors,
903    pub refresh: Refresh,
904    pub options: ResolverInstallerOptions,
905    pub settings: ResolverInstallerSettings,
906    pub env_file: Vec<PathBuf>,
907    pub no_env_file: bool,
908}
909
910impl ToolRunSettings {
911    /// Resolve the [`ToolRunSettings`] from the CLI and filesystem configuration.
912    pub fn resolve(
913        args: ToolRunArgs,
914        filesystem: Option<FilesystemOptions>,
915        invocation_source: ToolRunCommand,
916        environment: EnvironmentOptions,
917    ) -> anyhow::Result<Self> {
918        let ToolRunArgs {
919            command,
920            from,
921            with,
922            with_editable,
923            with_requirements,
924            constraints,
925            overrides,
926            build_constraints,
927            isolated,
928            env_file,
929            no_env_file,
930            show_resolution,
931            installer,
932            build,
933            refresh,
934            lfs,
935            python,
936            python_platform,
937            torch_backend,
938            generate_shell_completion: _,
939        } = args;
940
941        // If `--upgrade` was passed explicitly, warn.
942        if installer.upgrade || !installer.upgrade_package.is_empty() {
943            if with.is_empty() && with_requirements.is_empty() {
944                warn_user_once!(
945                    "Tools cannot be upgraded via `{invocation_source}`; use `uv tool upgrade --all` to upgrade all installed tools, or `{invocation_source} package@latest` to run the latest version of a tool."
946                );
947            } else {
948                warn_user_once!(
949                    "Tools cannot be upgraded via `{invocation_source}`; use `uv tool upgrade --all` to upgrade all installed tools, `{invocation_source} package@latest` to run the latest version of a tool, or `{invocation_source} --refresh package` to upgrade any `--with` dependencies."
950                );
951            }
952        }
953
954        // If `--reinstall` was passed explicitly, warn.
955        if installer.reinstall.reinstall || !installer.reinstall.reinstall_package.is_empty() {
956            if with.is_empty() && with_requirements.is_empty() {
957                warn_user_once!(
958                    "Tools cannot be reinstalled via `{invocation_source}`; use `uv tool upgrade --all --reinstall` to reinstall all installed tools, `{invocation_source} package@latest` to run the latest version of a tool, or `uv cache prune` to clear any cached tool environments."
959                );
960            } else {
961                warn_user_once!(
962                    "Tools cannot be reinstalled via `{invocation_source}`; use `uv tool upgrade --all --reinstall` to reinstall all installed tools, `{invocation_source} package@latest` to run the latest version of a tool, `{invocation_source} --refresh package` to reinstall any `--with` dependencies, or `uv cache prune` to clear any cached tool environments."
963                );
964            }
965        }
966
967        let filesystem_options = filesystem.map(FilesystemOptions::into_options);
968
969        let options = resolver_installer_options_with_environment(
970            resolver_installer_options(
971                installer,
972                build,
973                filesystem_options
974                    .as_ref()
975                    .and_then(|options| options.top_level.index.as_deref())
976                    .unwrap_or_default(),
977            )?,
978            &environment,
979        )
980        .combine(ResolverInstallerOptions::from(
981            filesystem_options
982                .as_ref()
983                .map(|options| options.top_level.clone())
984                .unwrap_or_default(),
985        ));
986
987        let filesystem_install_mirrors = filesystem_options
988            .map(|options| options.install_mirrors.clone())
989            .unwrap_or_default();
990
991        let mut settings = ResolverInstallerSettings::from(options.clone());
992        if torch_backend.is_some() {
993            settings.resolver.torch_backend = torch_backend;
994        }
995        let lfs = GitLfsSetting::new(lfs.then_some(true), environment.lfs);
996
997        // Resolve flags from CLI and environment variables.
998        let isolated = isolated || environment.isolated.value == Some(true);
999        let show_resolution = show_resolution || environment.show_resolution.value == Some(true);
1000        let no_env_file = no_env_file || environment.no_env_file.value == Some(true);
1001
1002        Ok(Self {
1003            command: command.map(|ExternalCommand::Cmd(command)| command),
1004            from,
1005            with: with
1006                .into_iter()
1007                .flat_map(CommaSeparatedRequirements::into_iter)
1008                .collect(),
1009            with_editable: with_editable
1010                .into_iter()
1011                .flat_map(CommaSeparatedRequirements::into_iter)
1012                .collect(),
1013            with_requirements: with_requirements
1014                .into_iter()
1015                .filter_map(Maybe::into_option)
1016                .collect(),
1017            constraints: requirement_files(constraints, environment.constraints.clone()),
1018            overrides: requirement_files(overrides, environment.overrides.clone()),
1019            build_constraints: requirement_files(
1020                build_constraints,
1021                environment.build_constraints.clone(),
1022            ),
1023            isolated,
1024            show_resolution,
1025            lfs,
1026            python: python.and_then(Maybe::into_option),
1027            python_platform,
1028            refresh: Refresh::try_from(refresh)?,
1029            settings,
1030            options,
1031            install_mirrors: environment
1032                .install_mirrors
1033                .combine(filesystem_install_mirrors),
1034            env_file,
1035            no_env_file,
1036        })
1037    }
1038}
1039
1040/// The resolved settings to use for a `tool install` invocation.
1041#[derive(Debug, Clone)]
1042pub struct ToolInstallSettings {
1043    pub package: String,
1044    pub from: Option<String>,
1045    pub with: Vec<String>,
1046    pub with_requirements: Vec<RequirementsInput>,
1047    pub with_executables_from: Vec<String>,
1048    pub with_editable: Vec<String>,
1049    pub constraints: Vec<RequirementsInput>,
1050    pub overrides: Vec<RequirementsInput>,
1051    pub excludes: Vec<RequirementsInput>,
1052    pub build_constraints: Vec<RequirementsInput>,
1053    pub lfs: GitLfsSetting,
1054    pub python: Option<String>,
1055    pub python_platform: Option<TargetTriple>,
1056    pub refresh: Refresh,
1057    pub options: ResolverInstallerOptions,
1058    pub settings: ResolverInstallerSettings,
1059    pub force: bool,
1060    pub editable: bool,
1061    pub install_mirrors: PythonInstallMirrors,
1062}
1063
1064impl ToolInstallSettings {
1065    /// Resolve the [`ToolInstallSettings`] from the CLI and filesystem configuration.
1066    pub fn resolve(
1067        args: ToolInstallArgs,
1068        filesystem: Option<FilesystemOptions>,
1069        environment: EnvironmentOptions,
1070    ) -> anyhow::Result<Self> {
1071        let ToolInstallArgs {
1072            package,
1073            editable,
1074            from,
1075            with,
1076            with_editable,
1077            with_requirements,
1078            with_executables_from,
1079            constraints:
1080                DependencyConstraintsArgs {
1081                    constraints,
1082                    overrides,
1083                    excludes,
1084                    build_constraints,
1085                },
1086            lfs,
1087            installer,
1088            force,
1089            build,
1090            refresh,
1091            python,
1092            python_platform,
1093            torch_backend,
1094        } = args;
1095
1096        let filesystem_options = filesystem.map(FilesystemOptions::into_options);
1097
1098        let options = resolver_installer_options_with_environment(
1099            resolver_installer_options(
1100                installer,
1101                build,
1102                filesystem_options
1103                    .as_ref()
1104                    .and_then(|options| options.top_level.index.as_deref())
1105                    .unwrap_or_default(),
1106            )?,
1107            &environment,
1108        )
1109        .combine(ResolverInstallerOptions::from(
1110            filesystem_options
1111                .as_ref()
1112                .map(|options| options.top_level.clone())
1113                .unwrap_or_default(),
1114        ));
1115
1116        let filesystem_install_mirrors = filesystem_options
1117            .map(|options| options.install_mirrors.clone())
1118            .unwrap_or_default();
1119
1120        let mut settings = ResolverInstallerSettings::from(options.clone());
1121        if torch_backend.is_some() {
1122            settings.resolver.torch_backend = torch_backend;
1123        }
1124        let lfs = GitLfsSetting::new(lfs.then_some(true), environment.lfs);
1125
1126        Ok(Self {
1127            package,
1128            from,
1129            with: with
1130                .into_iter()
1131                .flat_map(CommaSeparatedRequirements::into_iter)
1132                .collect(),
1133            with_editable: with_editable
1134                .into_iter()
1135                .flat_map(CommaSeparatedRequirements::into_iter)
1136                .collect(),
1137            with_requirements: with_requirements
1138                .into_iter()
1139                .filter_map(Maybe::into_option)
1140                .collect(),
1141            with_executables_from: with_executables_from
1142                .into_iter()
1143                .flat_map(CommaSeparatedRequirements::into_iter)
1144                .collect(),
1145            constraints: requirement_files(constraints, environment.constraints.clone()),
1146            overrides: requirement_files(overrides, environment.overrides.clone()),
1147            excludes: requirement_files(excludes, environment.excludes.clone()),
1148            build_constraints: requirement_files(
1149                build_constraints,
1150                environment.build_constraints.clone(),
1151            ),
1152            lfs,
1153            python: python.and_then(Maybe::into_option),
1154            python_platform,
1155            force,
1156            editable,
1157            refresh: Refresh::try_from(refresh)?,
1158            options,
1159            settings,
1160            install_mirrors: environment
1161                .install_mirrors
1162                .combine(filesystem_install_mirrors),
1163        })
1164    }
1165}
1166
1167/// The resolved settings to use for a `tool upgrade` invocation.
1168#[derive(Debug, Clone)]
1169pub struct ToolUpgradeSettings {
1170    pub names: Vec<String>,
1171    pub python: Option<String>,
1172    pub python_platform: Option<TargetTriple>,
1173    pub install_mirrors: PythonInstallMirrors,
1174    pub args: ResolverInstallerOptions,
1175    pub filesystem: ResolverInstallerOptions,
1176}
1177impl ToolUpgradeSettings {
1178    /// Resolve the [`ToolUpgradeSettings`] from the CLI and filesystem configuration.
1179    pub fn resolve(
1180        args: ToolUpgradeArgs,
1181        filesystem: Option<FilesystemOptions>,
1182        environment: &EnvironmentOptions,
1183    ) -> anyhow::Result<Self> {
1184        let ToolUpgradeArgs {
1185            name,
1186            python,
1187            python_platform,
1188            upgrade,
1189            upgrade_package,
1190            upgrade_group,
1191            index_args,
1192            all,
1193            reinstall,
1194            registry_client,
1195            version_selection,
1196            config_setting,
1197            config_setting_package: config_settings_package,
1198            build_isolation,
1199            exclude_newer,
1200            link_mode,
1201            compile_bytecode,
1202            sources,
1203            build,
1204        } = args;
1205
1206        if upgrade {
1207            warn_user_once!("`--upgrade` is enabled by default on `uv tool upgrade`");
1208        }
1209        if !upgrade_package.is_empty() {
1210            warn_user_once!("`--upgrade-package` is enabled by default on `uv tool upgrade`");
1211        }
1212
1213        // Enable `--upgrade` by default.
1214        let installer = ResolverInstallerArgs {
1215            index_args,
1216            upgrade: upgrade_package.is_empty(),
1217            no_upgrade: false,
1218            upgrade_package,
1219            upgrade_group,
1220            reinstall,
1221            registry_client,
1222            version_selection,
1223            config_setting,
1224            config_settings_package,
1225            build_isolation,
1226            exclude_newer,
1227            link_mode,
1228            compile_bytecode,
1229            sources,
1230        };
1231
1232        let args = resolver_installer_options_with_environment(
1233            resolver_installer_options(installer, build, configured_indexes(filesystem.as_ref()))?,
1234            environment,
1235        );
1236        let filesystem = filesystem.map(FilesystemOptions::into_options);
1237        let filesystem_install_mirrors = filesystem
1238            .as_ref()
1239            .map(|options| options.install_mirrors.clone())
1240            .unwrap_or_default();
1241        let top_level = ResolverInstallerOptions::from(
1242            filesystem
1243                .map(|options| options.top_level)
1244                .unwrap_or_default(),
1245        );
1246
1247        Ok(Self {
1248            names: if all { vec![] } else { name },
1249            python: python.and_then(Maybe::into_option),
1250            python_platform,
1251            args,
1252            filesystem: top_level,
1253            install_mirrors: environment
1254                .install_mirrors
1255                .clone()
1256                .combine(filesystem_install_mirrors),
1257        })
1258    }
1259}
1260
1261/// The resolved settings to use for a `tool list` invocation.
1262#[derive(Debug, Clone)]
1263pub struct ToolListSettings {
1264    pub show_paths: bool,
1265    pub show_version_specifiers: bool,
1266    pub show_with: bool,
1267    pub show_extras: bool,
1268    pub show_python: bool,
1269    pub outdated: bool,
1270    pub args: ResolverInstallerOptions,
1271    pub filesystem: ResolverInstallerOptions,
1272}
1273
1274impl ToolListSettings {
1275    /// Resolve the [`ToolListSettings`] from the CLI and filesystem configuration.
1276    pub fn resolve(
1277        args: ToolListArgs,
1278        filesystem: Option<FilesystemOptions>,
1279    ) -> anyhow::Result<Self> {
1280        let ToolListArgs {
1281            show_paths,
1282            show_version_specifiers,
1283            show_with,
1284            show_extras,
1285            show_python,
1286            outdated,
1287            no_outdated,
1288            exclude_newer:
1289                PackageExcludeNewerArgs {
1290                    exclude_newer: ExcludeNewerArgs { exclude_newer },
1291                    exclude_newer_package,
1292                },
1293            python_preference: _,
1294            no_python_downloads: _,
1295        } = args;
1296
1297        let top_level = filesystem
1298            .map(FilesystemOptions::into_options)
1299            .map(|options| options.top_level)
1300            .unwrap_or_default();
1301        let filesystem = ResolverInstallerOptions {
1302            indexes: IndexOptions {
1303                index: top_level.index,
1304                ..IndexOptions::default()
1305            },
1306            exclude_newer: top_level.exclude_newer,
1307            exclude_newer_package: top_level.exclude_newer_package,
1308            ..ResolverInstallerOptions::default()
1309        };
1310
1311        Ok(Self {
1312            show_paths,
1313            show_version_specifiers,
1314            show_with,
1315            show_extras,
1316            show_python,
1317            outdated: flag(outdated, no_outdated, "outdated")?.unwrap_or(false),
1318            args: ResolverInstallerOptions {
1319                exclude_newer,
1320                exclude_newer_package: exclude_newer_package.map(ExcludeNewerPackage::from_iter),
1321                ..ResolverInstallerOptions::default()
1322            },
1323            filesystem,
1324        })
1325    }
1326}
1327
1328/// The resolved settings to use for a `tool audit` invocation.
1329#[derive(Debug, Clone)]
1330pub struct ToolAuditSettings {
1331    pub names: Vec<PackageName>,
1332    pub output_format: AuditOutputFormat,
1333    pub service_format: VulnerabilityServiceFormat,
1334    pub service_url: Option<SafeUrl>,
1335    pub ignore: Vec<VulnerabilityID>,
1336    pub ignore_until_fixed: Vec<VulnerabilityID>,
1337    pub filesystem: ResolverInstallerOptions,
1338}
1339
1340impl ToolAuditSettings {
1341    /// Resolve the [`ToolAuditSettings`] from the CLI and user-level configuration.
1342    pub fn resolve(args: ToolAuditArgs, filesystem: Option<FilesystemOptions>) -> Self {
1343        let ToolAuditArgs {
1344            name,
1345            all,
1346            audit:
1347                AuditCommonArgs {
1348                    offline: _,
1349                    output_format,
1350                    ignore,
1351                    ignore_until_fixed,
1352                    service_format,
1353                    service_url,
1354                },
1355        } = args;
1356
1357        let audit = filesystem
1358            .as_ref()
1359            .and_then(|options| options.audit.clone())
1360            .unwrap_or_default();
1361        let filesystem = filesystem
1362            .map(FilesystemOptions::into_options)
1363            .map(|options| ResolverInstallerOptions::from(options.top_level))
1364            .unwrap_or_default();
1365
1366        let ignore = ignore
1367            .into_iter()
1368            .chain(audit.ignore.unwrap_or_default())
1369            .map(VulnerabilityID::new)
1370            .collect();
1371        let ignore_until_fixed = ignore_until_fixed
1372            .into_iter()
1373            .chain(audit.ignore_until_fixed.unwrap_or_default())
1374            .map(VulnerabilityID::new)
1375            .collect();
1376
1377        Self {
1378            names: if all { vec![] } else { name },
1379            output_format,
1380            service_format,
1381            service_url,
1382            ignore,
1383            ignore_until_fixed,
1384            filesystem,
1385        }
1386    }
1387}
1388
1389/// The resolved settings to use for a `tool uninstall` invocation.
1390#[derive(Debug, Clone)]
1391pub struct ToolUninstallSettings {
1392    pub name: Vec<PackageName>,
1393}
1394
1395impl ToolUninstallSettings {
1396    /// Resolve the [`ToolUninstallSettings`] from the CLI and filesystem configuration.
1397    pub fn resolve(args: ToolUninstallArgs, _filesystem: Option<FilesystemOptions>) -> Self {
1398        let ToolUninstallArgs { name, all } = args;
1399
1400        Self {
1401            name: if all { vec![] } else { name },
1402        }
1403    }
1404}
1405
1406/// The resolved settings to use for a `tool dir` invocation.
1407#[derive(Debug, Clone)]
1408pub struct ToolDirSettings {
1409    pub bin: bool,
1410}
1411
1412impl ToolDirSettings {
1413    /// Resolve the [`ToolDirSettings`] from the CLI and filesystem configuration.
1414    #[expect(clippy::needless_pass_by_value)]
1415    pub fn resolve(args: ToolDirArgs, _filesystem: Option<FilesystemOptions>) -> Self {
1416        let ToolDirArgs { bin } = args;
1417
1418        Self { bin }
1419    }
1420}
1421
1422/// The resolved settings to use for a `tool run` invocation.
1423#[derive(Debug, Clone)]
1424pub struct PythonListSettings {
1425    pub request: Option<String>,
1426    pub kinds: PythonListKinds,
1427    pub all_platforms: bool,
1428    pub all_arches: bool,
1429    pub all_versions: bool,
1430    pub show_urls: bool,
1431    pub output_format: PythonListFormat,
1432    pub install_mirrors: PythonInstallMirrors,
1433}
1434
1435impl PythonListSettings {
1436    /// Resolve the [`PythonListSettings`] from the CLI and filesystem configuration.
1437    pub fn resolve(
1438        args: PythonListArgs,
1439        filesystem: Option<FilesystemOptions>,
1440        environment: EnvironmentOptions,
1441    ) -> Self {
1442        let PythonListArgs {
1443            request,
1444            all_versions,
1445            all_platforms,
1446            all_arches,
1447            only_installed,
1448            only_downloads,
1449            show_urls,
1450            output_format,
1451            python_downloads_json_url: python_downloads_json_url_arg,
1452        } = args;
1453
1454        let filesystem_install_mirrors = filesystem
1455            .map(|fs| fs.install_mirrors.clone())
1456            .unwrap_or_default();
1457
1458        let install_mirrors = PythonInstallMirrors {
1459            python_downloads_json_url: python_downloads_json_url_arg,
1460            ..Default::default()
1461        }
1462        .combine(environment.install_mirrors)
1463        .combine(filesystem_install_mirrors);
1464
1465        let kinds = if only_installed {
1466            PythonListKinds::Installed
1467        } else if only_downloads {
1468            PythonListKinds::Downloads
1469        } else {
1470            PythonListKinds::default()
1471        };
1472
1473        Self {
1474            request,
1475            kinds,
1476            all_platforms,
1477            all_arches,
1478            all_versions,
1479            show_urls,
1480            output_format,
1481            install_mirrors,
1482        }
1483    }
1484}
1485
1486/// The resolved settings to use for a `python dir` invocation.
1487#[derive(Debug, Clone)]
1488pub struct PythonDirSettings {
1489    pub bin: bool,
1490}
1491
1492impl PythonDirSettings {
1493    /// Resolve the [`PythonDirSettings`] from the CLI and filesystem configuration.
1494    #[expect(clippy::needless_pass_by_value)]
1495    pub fn resolve(args: PythonDirArgs, _filesystem: Option<FilesystemOptions>) -> Self {
1496        let PythonDirArgs { bin } = args;
1497
1498        Self { bin }
1499    }
1500}
1501
1502/// The resolved settings to use for a `python install` invocation.
1503#[derive(Debug, Clone)]
1504pub struct PythonInstallSettings {
1505    pub install_dir: Option<PathBuf>,
1506    pub targets: Vec<String>,
1507    pub reinstall: bool,
1508    pub force: bool,
1509    pub upgrade: PythonUpgrade,
1510    pub bin: Option<bool>,
1511    pub registry: Option<bool>,
1512    pub install_mirrors: PythonInstallMirrors,
1513    pub default: bool,
1514    pub compile_bytecode: bool,
1515}
1516
1517impl PythonInstallSettings {
1518    /// Resolve the [`PythonInstallSettings`] from the CLI and filesystem configuration.
1519    pub fn resolve(
1520        args: PythonInstallArgs,
1521        filesystem: Option<FilesystemOptions>,
1522        environment: EnvironmentOptions,
1523    ) -> anyhow::Result<Self> {
1524        let filesystem_install_mirrors = filesystem
1525            .map(|fs| fs.install_mirrors.clone())
1526            .unwrap_or_default();
1527
1528        let install_mirrors = args
1529            .install_mirrors()
1530            .combine(environment.install_mirrors)
1531            .combine(filesystem_install_mirrors);
1532
1533        let PythonInstallArgs {
1534            install_dir,
1535            targets,
1536            reinstall,
1537            bin,
1538            no_bin,
1539            registry,
1540            no_registry,
1541            force,
1542            upgrade,
1543            mirror: _,
1544            pypy_mirror: _,
1545            graalpy_mirror: _,
1546            pyodide_mirror: _,
1547            python_downloads_json_url: _,
1548            default,
1549            compile_bytecode,
1550        } = args;
1551
1552        Ok(Self {
1553            install_dir,
1554            targets,
1555            reinstall,
1556            force,
1557            upgrade: if upgrade {
1558                PythonUpgrade::Enabled(PythonUpgradeSource::Install)
1559            } else {
1560                PythonUpgrade::Disabled
1561            },
1562            bin: flag(bin, no_bin, "bin")?.or(environment.python_install_bin),
1563            registry: match flag(registry, no_registry, "registry")? {
1564                Some(registry) => Some(registry),
1565                None => environment.python_install_registry.or(
1566                    if environment.python_no_registry.value == Some(true) {
1567                        Some(false)
1568                    } else {
1569                        None
1570                    },
1571                ),
1572            },
1573            install_mirrors,
1574            default,
1575            compile_bytecode: flag(
1576                compile_bytecode.compile_bytecode,
1577                compile_bytecode.no_compile_bytecode,
1578                "compile-bytecode",
1579            )?
1580            .unwrap_or_default(),
1581        })
1582    }
1583}
1584
1585/// The resolved settings to use for a `python upgrade` invocation.
1586#[expect(clippy::struct_excessive_bools)]
1587#[derive(Debug, Clone)]
1588pub struct PythonUpgradeSettings {
1589    pub install_dir: Option<PathBuf>,
1590    pub targets: Vec<String>,
1591    pub force: bool,
1592    pub registry: Option<bool>,
1593    pub install_mirrors: PythonInstallMirrors,
1594    pub reinstall: bool,
1595    pub default: bool,
1596    pub bin: Option<bool>,
1597    pub compile_bytecode: bool,
1598}
1599
1600impl PythonUpgradeSettings {
1601    /// Resolve the [`PythonUpgradeSettings`] from the CLI and filesystem configuration.
1602    pub fn resolve(
1603        args: PythonUpgradeArgs,
1604        filesystem: Option<FilesystemOptions>,
1605        environment: EnvironmentOptions,
1606    ) -> anyhow::Result<Self> {
1607        let filesystem_install_mirrors = filesystem
1608            .map(|fs| fs.install_mirrors.clone())
1609            .unwrap_or_default();
1610
1611        let install_mirrors = args
1612            .install_mirrors()
1613            .combine(environment.install_mirrors)
1614            .combine(filesystem_install_mirrors);
1615
1616        let force = false;
1617        let default = false;
1618        let bin = None;
1619        let registry = environment.python_install_registry.or(
1620            if environment.python_no_registry.value == Some(true) {
1621                Some(false)
1622            } else {
1623                None
1624            },
1625        );
1626
1627        let PythonUpgradeArgs {
1628            install_dir,
1629            targets,
1630            mirror: _,
1631            pypy_mirror: _,
1632            graalpy_mirror: _,
1633            pyodide_mirror: _,
1634            reinstall,
1635            python_downloads_json_url: _,
1636            compile_bytecode,
1637        } = args;
1638
1639        Ok(Self {
1640            install_dir,
1641            targets,
1642            force,
1643            registry,
1644            install_mirrors,
1645            reinstall,
1646            default,
1647            bin,
1648            compile_bytecode: flag(
1649                compile_bytecode.compile_bytecode,
1650                compile_bytecode.no_compile_bytecode,
1651                "compile-bytecode",
1652            )?
1653            .unwrap_or_default(),
1654        })
1655    }
1656}
1657
1658/// The resolved settings to use for a `python uninstall` invocation.
1659#[derive(Debug, Clone)]
1660pub struct PythonUninstallSettings {
1661    pub install_dir: Option<PathBuf>,
1662    pub targets: Vec<String>,
1663    pub all: bool,
1664}
1665
1666impl PythonUninstallSettings {
1667    /// Resolve the [`PythonUninstallSettings`] from the CLI and filesystem configuration.
1668    pub fn resolve(args: PythonUninstallArgs, _filesystem: Option<FilesystemOptions>) -> Self {
1669        let PythonUninstallArgs {
1670            install_dir,
1671            targets,
1672            all,
1673        } = args;
1674
1675        Self {
1676            install_dir,
1677            targets,
1678            all,
1679        }
1680    }
1681}
1682
1683/// The resolved settings to use for a `python find` invocation.
1684#[derive(Debug, Clone)]
1685pub struct PythonFindSettings {
1686    pub request: Option<String>,
1687    pub show_version: bool,
1688    pub resolve_links: bool,
1689    pub no_project: bool,
1690    pub system: bool,
1691    pub python_downloads_json_url: Option<String>,
1692}
1693
1694impl PythonFindSettings {
1695    /// Resolve the [`PythonFindSettings`] from the CLI and workspace configuration.
1696    pub fn resolve(
1697        args: PythonFindArgs,
1698        filesystem: Option<FilesystemOptions>,
1699        environment: EnvironmentOptions,
1700    ) -> anyhow::Result<Self> {
1701        let PythonFindArgs {
1702            request,
1703            show_version,
1704            resolve_links,
1705            no_project,
1706            system,
1707            no_system,
1708            script: _,
1709            python_downloads_json_url,
1710        } = args;
1711
1712        let filesystem_install_mirrors = filesystem
1713            .map(|fs| fs.install_mirrors.clone())
1714            .unwrap_or_default();
1715
1716        let install_mirrors = PythonInstallMirrors {
1717            python_downloads_json_url,
1718            ..Default::default()
1719        }
1720        .combine(environment.install_mirrors)
1721        .combine(filesystem_install_mirrors);
1722
1723        let PythonInstallMirrors {
1724            python_install_mirror: _,
1725            pypy_install_mirror: _,
1726            graalpy_install_mirror: _,
1727            pyodide_install_mirror: _,
1728            python_downloads_json_url,
1729        } = install_mirrors;
1730
1731        Ok(Self {
1732            request,
1733            show_version,
1734            resolve_links,
1735            no_project,
1736            system: flag(system, no_system, "system")?.unwrap_or_default(),
1737            python_downloads_json_url,
1738        })
1739    }
1740}
1741
1742/// The resolved settings to use for a `python pin` invocation.
1743#[derive(Debug, Clone)]
1744pub struct PythonPinSettings {
1745    pub request: Option<String>,
1746    pub resolved: bool,
1747    pub no_project: bool,
1748    pub global: bool,
1749    pub rm: bool,
1750    pub install_mirrors: PythonInstallMirrors,
1751}
1752
1753impl PythonPinSettings {
1754    /// Resolve the [`PythonPinSettings`] from the CLI and workspace configuration.
1755    pub fn resolve(
1756        args: PythonPinArgs,
1757        filesystem: Option<FilesystemOptions>,
1758        environment: EnvironmentOptions,
1759    ) -> anyhow::Result<Self> {
1760        let PythonPinArgs {
1761            request,
1762            no_resolved,
1763            resolved,
1764            no_project,
1765            global,
1766            rm,
1767            python_downloads_json_url,
1768        } = args;
1769
1770        let filesystem_install_mirrors = filesystem
1771            .map(|fs| fs.install_mirrors.clone())
1772            .unwrap_or_default();
1773
1774        let install_mirrors = PythonInstallMirrors {
1775            python_downloads_json_url,
1776            ..Default::default()
1777        }
1778        .combine(environment.install_mirrors)
1779        .combine(filesystem_install_mirrors);
1780
1781        Ok(Self {
1782            request,
1783            resolved: flag(resolved, no_resolved, "resolved")?.unwrap_or(false),
1784            no_project,
1785            global,
1786            rm,
1787            install_mirrors,
1788        })
1789    }
1790}
1791
1792/// The resolved settings to use for a `sync` invocation.
1793#[derive(Debug, Clone)]
1794pub struct SyncSettings {
1795    pub lock_check: LockCheck,
1796    pub frozen: Option<FrozenSource>,
1797    pub dry_run: DryRun,
1798    pub script: Option<PathBuf>,
1799    pub active: ActiveEnvironment,
1800    pub extras: ExtrasSpecification,
1801    pub groups: DependencyGroups,
1802    pub editable: Option<EditableMode>,
1803    pub install_options: InstallOptions,
1804    pub modifications: Modifications,
1805    pub all_packages: bool,
1806    pub package: Vec<PackageName>,
1807    pub python: Option<String>,
1808    pub python_platform: Option<TargetTriple>,
1809    pub install_mirrors: PythonInstallMirrors,
1810    pub refresh: Refresh,
1811    pub settings: ResolverInstallerSettings,
1812    pub output_format: SyncFormat,
1813    pub malware_settings: MalwareCheckSettings,
1814}
1815
1816impl SyncSettings {
1817    /// Resolve the [`SyncSettings`] from the CLI and filesystem configuration.
1818    pub fn resolve(
1819        args: SyncArgs,
1820        filesystem: Option<FilesystemOptions>,
1821        environment: EnvironmentOptions,
1822    ) -> anyhow::Result<Self> {
1823        let SyncArgs {
1824            extra,
1825            all_extras,
1826            no_extra,
1827            no_all_extras,
1828            dependency_groups:
1829                ProjectDependencyGroupsArgs {
1830                    dev,
1831                    no_dev,
1832                    only_dev,
1833                    group,
1834                    no_group,
1835                    no_default_groups,
1836                    only_group,
1837                    all_groups,
1838                },
1839            editable,
1840            no_editable,
1841            no_editable_package,
1842            inexact,
1843            exact,
1844            no_install_project,
1845            only_install_project,
1846            no_install_workspace,
1847            only_install_workspace,
1848            no_install_local,
1849            only_install_local,
1850            no_install_package,
1851            only_install_package,
1852            locked,
1853            no_locked,
1854            frozen,
1855            no_frozen,
1856            active,
1857            no_active,
1858            dry_run,
1859            installer,
1860            build,
1861            refresh,
1862            all_packages,
1863            package,
1864            script,
1865            python,
1866            python_platform,
1867            check,
1868            no_check,
1869            output_format,
1870        } = args;
1871        let filesystem_install_mirrors = filesystem
1872            .as_ref()
1873            .map(|fs| fs.install_mirrors.clone())
1874            .unwrap_or_default();
1875
1876        let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment);
1877        let settings =
1878            resolve_resolver_installer_settings(installer, build, filesystem, &environment)?;
1879
1880        let check = flag(check, no_check, "check")?.unwrap_or_default();
1881        let dry_run = if check {
1882            DryRun::Check
1883        } else {
1884            DryRun::from_args(dry_run)
1885        };
1886
1887        // Resolve flags from CLI and environment variables.
1888        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
1889        let frozen = resolve_frozen(frozen, no_frozen, FrozenFlag::Frozen, environment.frozen);
1890
1891        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
1892
1893        let (dev, no_dev) = resolve_flag_pair(
1894            dev,
1895            no_dev,
1896            "dev",
1897            "no-dev",
1898            Some(environment.dev),
1899            Some(environment.no_dev),
1900        );
1901        let (editable, no_editable) = resolve_flag_pair(
1902            editable,
1903            no_editable,
1904            "editable",
1905            "no-editable",
1906            None,
1907            Some(environment.no_editable),
1908        );
1909
1910        let (no_install_project, only_install_project) = resolve_flag_pair(
1911            no_install_project,
1912            only_install_project,
1913            "no-install-project",
1914            "only-install-project",
1915            Some(environment.no_install_project),
1916            Some(environment.only_install_project),
1917        );
1918        let (no_install_workspace, only_install_workspace) = resolve_flag_pair(
1919            no_install_workspace,
1920            only_install_workspace,
1921            "no-install-workspace",
1922            "only-install-workspace",
1923            Some(environment.no_install_workspace),
1924            Some(environment.only_install_workspace),
1925        );
1926        let (no_install_local, only_install_local) = resolve_flag_pair(
1927            no_install_local,
1928            only_install_local,
1929            "no-install-local",
1930            "only-install-local",
1931            Some(environment.no_install_local),
1932            Some(environment.only_install_local),
1933        );
1934        check_conflicts(no_install_project, only_install_project)?;
1935        check_conflicts(no_install_workspace, only_install_workspace)?;
1936        check_conflicts(no_install_local, only_install_local)?;
1937        if script.is_some() {
1938            let script = Flag::from_cli("script");
1939            check_conflicts(no_install_project, script)?;
1940            check_conflicts(no_install_workspace, script)?;
1941            check_conflicts(no_install_local, script)?;
1942        }
1943        let no_install_project = no_install_project.is_enabled();
1944        let only_install_project = only_install_project.is_enabled();
1945        let no_install_workspace = no_install_workspace.is_enabled();
1946        let only_install_workspace = only_install_workspace.is_enabled();
1947        let no_install_local = no_install_local.is_enabled();
1948        let only_install_local = only_install_local.is_enabled();
1949
1950        Ok(Self {
1951            output_format,
1952            lock_check: locked,
1953            frozen,
1954            dry_run,
1955            script,
1956            active: flag(active, no_active, "active")?.into(),
1957            extras: ExtrasSpecification::from_args(
1958                extra.unwrap_or_default(),
1959                no_extra,
1960                // TODO(blueraft): support no_default_extras
1961                false,
1962                // TODO(blueraft): support only_extra
1963                vec![],
1964                flag(all_extras, no_all_extras, "all-extras")?.unwrap_or_default(),
1965            ),
1966            groups: DependencyGroups::from_args(
1967                DevMode::from_args(dev.into(), no_dev.into(), only_dev),
1968                group,
1969                if no_group.is_empty() {
1970                    environment.no_group.clone().unwrap_or_default()
1971                } else {
1972                    no_group
1973                },
1974                no_default_groups,
1975                only_group,
1976                all_groups,
1977            ),
1978            editable: EditableMode::from_args(
1979                flag(editable.into(), no_editable.into(), "editable")?,
1980                no_editable_package,
1981            ),
1982            install_options: InstallOptions::new(
1983                no_install_project,
1984                only_install_project,
1985                no_install_workspace,
1986                only_install_workspace,
1987                no_install_local,
1988                only_install_local,
1989                no_install_package,
1990                only_install_package,
1991            ),
1992            modifications: if flag(exact, inexact, "inexact")?.unwrap_or(true) {
1993                Modifications::Exact
1994            } else {
1995                Modifications::Sufficient
1996            },
1997            all_packages,
1998            package,
1999            python: python.and_then(Maybe::into_option),
2000            python_platform,
2001            refresh: Refresh::try_from(refresh)?,
2002            settings,
2003            install_mirrors: environment
2004                .install_mirrors
2005                .combine(filesystem_install_mirrors),
2006            malware_settings,
2007        })
2008    }
2009}
2010
2011/// The resolved settings to use for a `lock` invocation.
2012#[derive(Debug, Clone)]
2013pub struct LockSettings {
2014    pub lock_check: LockCheck,
2015    pub frozen: Option<FrozenSource>,
2016    pub dry_run: DryRun,
2017    pub script: Option<PathBuf>,
2018    pub python: Option<String>,
2019    pub install_mirrors: PythonInstallMirrors,
2020    pub refresh: Refresh,
2021    pub settings: ResolverSettings,
2022}
2023
2024impl LockSettings {
2025    /// Resolve the [`LockSettings`] from the CLI and filesystem configuration.
2026    pub fn resolve(
2027        args: LockArgs,
2028        filesystem: Option<FilesystemOptions>,
2029        environment: EnvironmentOptions,
2030    ) -> anyhow::Result<Self> {
2031        let LockArgs {
2032            check,
2033            locked,
2034            no_locked,
2035            check_exists,
2036            frozen,
2037            no_frozen,
2038            dry_run,
2039            script,
2040            resolver,
2041            build,
2042            refresh,
2043            python,
2044        } = args;
2045
2046        let filesystem_install_mirrors = filesystem
2047            .as_ref()
2048            .map(|fs| fs.install_mirrors.clone())
2049            .unwrap_or_default();
2050
2051        // Resolve flags from CLI and environment variables.
2052        let locked = resolve_lock_check(
2053            locked || check,
2054            no_locked,
2055            if check {
2056                LockedFlag::Check
2057            } else {
2058                LockedFlag::Locked
2059            },
2060            environment.locked,
2061        );
2062        let frozen = resolve_frozen(
2063            frozen || check_exists,
2064            no_frozen,
2065            if check_exists {
2066                FrozenFlag::CheckExists
2067            } else {
2068                FrozenFlag::Frozen
2069            },
2070            environment.frozen,
2071        );
2072
2073        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
2074
2075        Ok(Self {
2076            lock_check: locked,
2077            frozen,
2078            dry_run: DryRun::from_args(dry_run),
2079            script,
2080            python: python.and_then(Maybe::into_option),
2081            refresh: Refresh::try_from(refresh)?,
2082            settings: resolve_resolver_settings(resolver, build, filesystem, &environment)?,
2083            install_mirrors: environment
2084                .install_mirrors
2085                .combine(filesystem_install_mirrors),
2086        })
2087    }
2088}
2089
2090/// The resolved settings to use for an `upgrade` invocation.
2091#[derive(Debug, Clone)]
2092pub struct UpgradeSettings {
2093    pub packages: Vec<PackageName>,
2094    pub exclude: Vec<PackageName>,
2095    pub install_mirrors: PythonInstallMirrors,
2096    pub settings: ResolverSettings,
2097}
2098
2099impl UpgradeSettings {
2100    /// Resolve the [`UpgradeSettings`] from the CLI and filesystem configuration.
2101    pub fn resolve(
2102        args: UpgradeArgs,
2103        filesystem: Option<FilesystemOptions>,
2104        environment: EnvironmentOptions,
2105    ) -> anyhow::Result<Self> {
2106        let filesystem_install_mirrors = filesystem
2107            .as_ref()
2108            .map(|fs| fs.install_mirrors.clone())
2109            .unwrap_or_default();
2110        let (packages, exclude, options) =
2111            upgrade_options(args, configured_indexes(filesystem.as_ref()))?;
2112        let mut settings = combine_resolver_settings(options, filesystem, &environment);
2113        settings.upgrade = if packages.is_empty() {
2114            Upgrade::default()
2115        } else {
2116            Upgrade::from_packages(packages.clone())
2117        };
2118
2119        Ok(Self {
2120            packages,
2121            exclude,
2122            install_mirrors: environment
2123                .install_mirrors
2124                .combine(filesystem_install_mirrors),
2125            settings,
2126        })
2127    }
2128}
2129
2130/// The resolved settings to use for a `lock` invocation.
2131#[derive(Debug, Clone)]
2132pub struct MetadataSettings {
2133    #[expect(dead_code)]
2134    script: Option<PathBuf>,
2135    pub lock_check: LockCheck,
2136    pub frozen: Option<FrozenSource>,
2137    pub sync: Option<Modifications>,
2138    pub active: ActiveEnvironment,
2139    pub python: Option<String>,
2140    pub install_mirrors: PythonInstallMirrors,
2141    pub refresh: Refresh,
2142    pub settings: ResolverSettings,
2143    pub malware_settings: MalwareCheckSettings,
2144}
2145
2146impl MetadataSettings {
2147    /// Resolve the [`LockSettings`] from the CLI and filesystem configuration.
2148    pub fn resolve(
2149        args: Box<MetadataArgs>,
2150        filesystem: Option<FilesystemOptions>,
2151        environment: EnvironmentOptions,
2152    ) -> anyhow::Result<Self> {
2153        let MetadataArgs {
2154            script,
2155            locked,
2156            no_locked,
2157            frozen,
2158            no_frozen,
2159            resolver,
2160            build,
2161            refresh,
2162            sync,
2163            exact,
2164            active,
2165            python,
2166        } = *args;
2167
2168        let filesystem_install_mirrors = filesystem
2169            .as_ref()
2170            .map(|fs| fs.install_mirrors.clone())
2171            .unwrap_or_default();
2172
2173        // Resolve flags from CLI and environment variables.
2174        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
2175        let frozen = resolve_frozen(frozen, no_frozen, FrozenFlag::Frozen, environment.frozen);
2176
2177        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
2178
2179        let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment);
2180
2181        Ok(Self {
2182            script,
2183            lock_check: locked,
2184            frozen,
2185            sync: sync.then_some(if exact {
2186                Modifications::Exact
2187            } else {
2188                Modifications::Sufficient
2189            }),
2190            active: Some(active).into(),
2191            python: python.and_then(Maybe::into_option),
2192            refresh: Refresh::try_from(refresh)?,
2193            settings: resolve_resolver_settings(resolver, build, filesystem, &environment)?,
2194            install_mirrors: environment
2195                .install_mirrors
2196                .combine(filesystem_install_mirrors),
2197            malware_settings,
2198        })
2199    }
2200}
2201
2202/// The resolved settings to use for a `add` invocation.
2203#[expect(clippy::struct_excessive_bools)]
2204#[derive(Debug, Clone)]
2205pub struct AddSettings {
2206    pub lock_check: LockCheck,
2207    pub frozen: Option<FrozenSource>,
2208    pub active: ActiveEnvironment,
2209    pub no_sync: bool,
2210    pub packages: Vec<String>,
2211    pub requirements: Vec<RequirementsInput>,
2212    pub constraints: Vec<RequirementsInput>,
2213    pub marker: Option<MarkerTree>,
2214    pub dependency_type: DependencyType,
2215    pub editable: Option<EditableMode>,
2216    pub extras: Vec<ExtraName>,
2217    pub raw: bool,
2218    pub bounds: Option<AddBoundsKind>,
2219    pub rev: Option<String>,
2220    pub tag: Option<String>,
2221    pub branch: Option<String>,
2222    pub lfs: GitLfsSetting,
2223    pub package: Option<PackageName>,
2224    pub script: Option<PathBuf>,
2225    pub python: Option<String>,
2226    pub workspace: Option<bool>,
2227    pub no_install_project: bool,
2228    pub only_install_project: bool,
2229    pub no_install_workspace: bool,
2230    pub only_install_workspace: bool,
2231    pub no_install_local: bool,
2232    pub only_install_local: bool,
2233    pub no_install_package: Vec<PackageName>,
2234    pub only_install_package: Vec<PackageName>,
2235    pub install_mirrors: PythonInstallMirrors,
2236    pub refresh: Refresh,
2237    pub indexes: Vec<Index>,
2238    pub settings: ResolverInstallerSettings,
2239    pub malware_settings: MalwareCheckSettings,
2240}
2241
2242impl AddSettings {
2243    /// Resolve the [`AddSettings`] from the CLI and filesystem configuration.
2244    pub fn resolve(
2245        args: AddArgs,
2246        filesystem: Option<FilesystemOptions>,
2247        environment: EnvironmentOptions,
2248    ) -> anyhow::Result<Self> {
2249        let AddArgs {
2250            packages,
2251            requirements,
2252            constraints,
2253            marker,
2254            dev,
2255            optional,
2256            group,
2257            editable,
2258            no_editable,
2259            no_editable_package,
2260            extra,
2261            raw,
2262            bounds,
2263            rev,
2264            tag,
2265            branch,
2266            lfs,
2267            no_sync,
2268            locked,
2269            no_locked,
2270            frozen,
2271            no_frozen,
2272            active,
2273            no_active,
2274            installer,
2275            build,
2276            refresh,
2277            package,
2278            script,
2279            python,
2280            workspace,
2281            no_workspace,
2282            no_install_project,
2283            only_install_project,
2284            no_install_workspace,
2285            only_install_workspace,
2286            no_install_local,
2287            only_install_local,
2288            no_install_package,
2289            only_install_package,
2290        } = args;
2291
2292        // Resolve flags from CLI and environment variables.
2293        let dev = dev || environment.dev.value == Some(true);
2294        let (editable, no_editable) = resolve_flag_pair(
2295            editable,
2296            no_editable,
2297            "editable",
2298            "no-editable",
2299            None,
2300            Some(environment.no_editable),
2301        );
2302
2303        let (no_install_project, only_install_project) = resolve_flag_pair(
2304            no_install_project,
2305            only_install_project,
2306            "no-install-project",
2307            "only-install-project",
2308            Some(environment.no_install_project),
2309            Some(environment.only_install_project),
2310        );
2311        let (no_install_workspace, only_install_workspace) = resolve_flag_pair(
2312            no_install_workspace,
2313            only_install_workspace,
2314            "no-install-workspace",
2315            "only-install-workspace",
2316            Some(environment.no_install_workspace),
2317            Some(environment.only_install_workspace),
2318        );
2319        let (no_install_local, only_install_local) = resolve_flag_pair(
2320            no_install_local,
2321            only_install_local,
2322            "no-install-local",
2323            "only-install-local",
2324            Some(environment.no_install_local),
2325            Some(environment.only_install_local),
2326        );
2327        check_conflicts(no_install_project, only_install_project)?;
2328        check_conflicts(no_install_workspace, only_install_workspace)?;
2329        check_conflicts(no_install_local, only_install_local)?;
2330
2331        let dependency_type = if let Some(extra) = optional {
2332            DependencyType::Optional(extra)
2333        } else if let Some(group) = group {
2334            DependencyType::Group(group)
2335        } else if dev {
2336            DependencyType::Dev
2337        } else {
2338            DependencyType::Production
2339        };
2340
2341        // If the user passed an `--index-url` or `--extra-index-url`, warn.
2342        if installer
2343            .index_args
2344            .index_url
2345            .as_ref()
2346            .is_some_and(Maybe::is_some)
2347        {
2348            if script.is_some() {
2349                warn_user_once!(
2350                    "Indexes specified via `--index-url` will not be persisted to the script; use `--default-index` instead."
2351                );
2352            } else {
2353                warn_user_once!(
2354                    "Indexes specified via `--index-url` will not be persisted to the `pyproject.toml` file; use `--default-index` instead."
2355                );
2356            }
2357        }
2358
2359        if installer
2360            .index_args
2361            .extra_index_url
2362            .as_ref()
2363            .is_some_and(|extra_index_url| extra_index_url.iter().any(Maybe::is_some))
2364        {
2365            if script.is_some() {
2366                warn_user_once!(
2367                    "Indexes specified via `--extra-index-url` will not be persisted to the script; use `--index` instead."
2368                );
2369            } else {
2370                warn_user_once!(
2371                    "Indexes specified via `--extra-index-url` will not be persisted to the `pyproject.toml` file; use `--index` instead."
2372                );
2373            }
2374        }
2375
2376        let filesystem_install_mirrors = filesystem
2377            .as_ref()
2378            .map(|fs| fs.install_mirrors.clone())
2379            .unwrap_or_default();
2380
2381        let bounds = bounds.or(filesystem.as_ref().and_then(|fs| fs.add.add_bounds));
2382        let lfs = GitLfsSetting::new(lfs.then_some(true), environment.lfs);
2383
2384        // Resolve flags from CLI and environment variables.
2385        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
2386        let frozen = resolve_frozen(frozen, no_frozen, FrozenFlag::Frozen, environment.frozen);
2387        let no_sync = resolve_flag(no_sync, "no-sync", environment.no_sync);
2388
2389        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
2390
2391        // Check for conflicts between no_sync and frozen.
2392        check_conflicts(no_sync, frozen.map_or(Flag::Disabled, Flag::from))?;
2393
2394        let no_install_package_flag = if no_install_package.is_empty() {
2395            Flag::disabled()
2396        } else {
2397            Flag::from_cli("no-install-package")
2398        };
2399        let only_install_package_flag = if only_install_package.is_empty() {
2400            Flag::disabled()
2401        } else {
2402            Flag::from_cli("only-install-package")
2403        };
2404
2405        for install_flag in [
2406            no_install_project,
2407            no_install_workspace,
2408            no_install_local,
2409            only_install_project,
2410            only_install_workspace,
2411            only_install_local,
2412            no_install_package_flag,
2413            only_install_package_flag,
2414        ] {
2415            check_conflicts(install_flag, frozen.map_or(Flag::Disabled, Flag::from))?;
2416            check_conflicts(install_flag, no_sync)?;
2417        }
2418
2419        let no_install_project = no_install_project.is_enabled();
2420        let only_install_project = only_install_project.is_enabled();
2421        let no_install_workspace = no_install_workspace.is_enabled();
2422        let only_install_workspace = only_install_workspace.is_enabled();
2423        let no_install_local = no_install_local.is_enabled();
2424        let only_install_local = only_install_local.is_enabled();
2425
2426        let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment);
2427        let active = flag(active, no_active, "active")?.into();
2428        let workspace = flag(workspace, no_workspace, "workspace")?;
2429        let editable = EditableMode::from_args(
2430            flag(editable.into(), no_editable.into(), "editable")?,
2431            no_editable_package,
2432        );
2433        let refresh = Refresh::try_from(refresh)?;
2434        let options =
2435            resolver_installer_options(installer, build, configured_indexes(filesystem.as_ref()))?;
2436        let indexes = options.indexes.index.clone().unwrap_or_default();
2437
2438        Ok(Self {
2439            lock_check: locked,
2440            frozen,
2441            active,
2442            no_sync: no_sync.is_enabled(),
2443            packages,
2444            requirements,
2445            constraints: requirement_files(constraints, environment.constraints.clone()),
2446            marker,
2447            dependency_type,
2448            raw,
2449            bounds,
2450            rev,
2451            tag,
2452            branch,
2453            lfs,
2454            package,
2455            script,
2456            python: python.and_then(Maybe::into_option),
2457            workspace,
2458            no_install_project,
2459            only_install_project,
2460            no_install_workspace,
2461            only_install_workspace,
2462            no_install_local,
2463            only_install_local,
2464            no_install_package,
2465            only_install_package,
2466            editable,
2467            extras: extra.unwrap_or_default(),
2468            refresh,
2469            indexes,
2470            settings: combine_resolver_installer_settings(options, filesystem, &environment),
2471            install_mirrors: environment
2472                .install_mirrors
2473                .combine(filesystem_install_mirrors),
2474            malware_settings,
2475        })
2476    }
2477}
2478
2479/// The resolved settings to use for a `remove` invocation.
2480#[derive(Debug, Clone)]
2481pub struct RemoveSettings {
2482    pub lock_check: LockCheck,
2483    pub frozen: Option<FrozenSource>,
2484    pub active: ActiveEnvironment,
2485    pub no_sync: bool,
2486    pub packages: Vec<PackageName>,
2487    pub dependency_type: DependencyType,
2488    pub package: Option<PackageName>,
2489    pub script: Option<PathBuf>,
2490    pub python: Option<String>,
2491    pub install_mirrors: PythonInstallMirrors,
2492    pub refresh: Refresh,
2493    pub settings: ResolverInstallerSettings,
2494    pub malware_settings: MalwareCheckSettings,
2495}
2496
2497impl RemoveSettings {
2498    /// Resolve the [`RemoveSettings`] from the CLI and filesystem configuration.
2499    pub fn resolve(
2500        args: RemoveArgs,
2501        filesystem: Option<FilesystemOptions>,
2502        environment: EnvironmentOptions,
2503    ) -> anyhow::Result<Self> {
2504        let RemoveArgs {
2505            dev,
2506            optional,
2507            packages,
2508            group,
2509            no_sync,
2510            locked,
2511            no_locked,
2512            frozen,
2513            no_frozen,
2514            active,
2515            no_active,
2516            installer,
2517            build,
2518            refresh,
2519            package,
2520            script,
2521            python,
2522        } = args;
2523
2524        // Resolve flags from CLI and environment variables.
2525        let dev = dev || environment.dev.value == Some(true);
2526
2527        let dependency_type = if let Some(extra) = optional {
2528            DependencyType::Optional(extra)
2529        } else if let Some(group) = group {
2530            DependencyType::Group(group)
2531        } else if dev {
2532            DependencyType::Dev
2533        } else {
2534            DependencyType::Production
2535        };
2536
2537        let filesystem_install_mirrors = filesystem
2538            .as_ref()
2539            .map(|fs| fs.install_mirrors.clone())
2540            .unwrap_or_default();
2541
2542        let packages = packages
2543            .into_iter()
2544            .map(|requirement| requirement.name)
2545            .collect();
2546
2547        // Resolve flags from CLI and environment variables.
2548        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
2549        let frozen = resolve_frozen(frozen, no_frozen, FrozenFlag::Frozen, environment.frozen);
2550        let no_sync = resolve_flag(no_sync, "no-sync", environment.no_sync);
2551
2552        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
2553
2554        // Check for conflicts between no_sync and frozen.
2555        check_conflicts(no_sync, frozen.map_or(Flag::Disabled, Flag::from))?;
2556
2557        let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment);
2558
2559        Ok(Self {
2560            lock_check: locked,
2561            frozen,
2562            active: flag(active, no_active, "active")?.into(),
2563            no_sync: no_sync.is_enabled(),
2564            packages,
2565            dependency_type,
2566            package,
2567            script,
2568            python: python.and_then(Maybe::into_option),
2569            refresh: Refresh::try_from(refresh)?,
2570            settings: resolve_resolver_installer_settings(
2571                installer,
2572                build,
2573                filesystem,
2574                &environment,
2575            )?,
2576            install_mirrors: environment
2577                .install_mirrors
2578                .combine(filesystem_install_mirrors),
2579            malware_settings,
2580        })
2581    }
2582}
2583
2584/// The resolved settings to use for a `version` invocation.
2585#[derive(Debug, Clone)]
2586pub struct VersionSettings {
2587    pub value: Option<String>,
2588    pub bump: Vec<VersionBumpSpec>,
2589    pub short: bool,
2590    pub output_format: VersionFormat,
2591    pub dry_run: bool,
2592    pub lock_check: LockCheck,
2593    pub frozen: Option<FrozenSource>,
2594    pub active: ActiveEnvironment,
2595    pub no_sync: bool,
2596    pub package: Option<PackageName>,
2597    pub python: Option<String>,
2598    pub install_mirrors: PythonInstallMirrors,
2599    pub refresh: Refresh,
2600    pub settings: ResolverInstallerSettings,
2601    pub malware_settings: MalwareCheckSettings,
2602}
2603
2604impl VersionSettings {
2605    /// Resolve the [`RemoveSettings`] from the CLI and filesystem configuration.
2606    pub fn resolve(
2607        args: VersionArgs,
2608        filesystem: Option<FilesystemOptions>,
2609        environment: EnvironmentOptions,
2610    ) -> anyhow::Result<Self> {
2611        let VersionArgs {
2612            value,
2613            bump,
2614            short,
2615            output_format,
2616            dry_run,
2617            no_sync,
2618            locked,
2619            no_locked,
2620            frozen,
2621            no_frozen,
2622            active,
2623            no_active,
2624            installer,
2625            build,
2626            refresh,
2627            package,
2628            python,
2629        } = args;
2630
2631        let filesystem_install_mirrors = filesystem
2632            .as_ref()
2633            .map(|fs| fs.install_mirrors.clone())
2634            .unwrap_or_default();
2635
2636        // Resolve flags from CLI and environment variables.
2637        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
2638        let frozen = resolve_frozen(frozen, no_frozen, FrozenFlag::Frozen, environment.frozen);
2639        let no_sync = resolve_flag(no_sync, "no-sync", environment.no_sync);
2640
2641        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
2642
2643        // Check for conflicts between no_sync and frozen.
2644        check_conflicts(no_sync, frozen.map_or(Flag::Disabled, Flag::from))?;
2645
2646        let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment);
2647
2648        Ok(Self {
2649            value,
2650            bump,
2651            short,
2652            output_format,
2653            dry_run,
2654            lock_check: locked,
2655            frozen,
2656            active: flag(active, no_active, "active")?.into(),
2657            no_sync: no_sync.is_enabled(),
2658            package,
2659            python: python.and_then(Maybe::into_option),
2660            refresh: Refresh::try_from(refresh)?,
2661            settings: resolve_resolver_installer_settings(
2662                installer,
2663                build,
2664                filesystem,
2665                &environment,
2666            )?,
2667            install_mirrors: environment
2668                .install_mirrors
2669                .combine(filesystem_install_mirrors),
2670            malware_settings,
2671        })
2672    }
2673}
2674
2675/// The resolved settings to use for a `tree` invocation.
2676#[derive(Debug, Clone)]
2677pub struct TreeSettings {
2678    pub groups: DependencyGroups,
2679    pub lock_check: LockCheck,
2680    pub frozen: Option<FrozenSource>,
2681    pub universal: bool,
2682    pub format: TreeFormat,
2683    pub depth: u8,
2684    pub prune: Vec<PackageName>,
2685    pub package: Vec<PackageName>,
2686    pub no_dedupe: bool,
2687    pub invert: bool,
2688    pub outdated: bool,
2689    pub show_sizes: bool,
2690    pub script: Option<PathBuf>,
2691    pub python_version: Option<PythonVersion>,
2692    pub python_platform: Option<TargetTriple>,
2693    pub python: Option<String>,
2694    pub install_mirrors: PythonInstallMirrors,
2695    pub resolver: ResolverSettings,
2696}
2697
2698impl TreeSettings {
2699    /// Resolve the [`TreeSettings`] from the CLI and workspace configuration.
2700    pub fn resolve(
2701        args: TreeArgs,
2702        filesystem: Option<FilesystemOptions>,
2703        environment: EnvironmentOptions,
2704    ) -> anyhow::Result<Self> {
2705        let TreeArgs {
2706            tree,
2707            universal,
2708            format,
2709            dependency_groups:
2710                ProjectDependencyGroupsArgs {
2711                    dev,
2712                    no_dev,
2713                    only_dev,
2714                    group,
2715                    no_group,
2716                    no_default_groups,
2717                    only_group,
2718                    all_groups,
2719                },
2720            locked,
2721            no_locked,
2722            frozen,
2723            no_frozen,
2724            build,
2725            resolver,
2726            script,
2727            python_version,
2728            python_platform,
2729            python,
2730        } = args;
2731
2732        let filesystem_install_mirrors = filesystem
2733            .as_ref()
2734            .map(|fs| fs.install_mirrors.clone())
2735            .unwrap_or_default();
2736
2737        // Resolve flags from CLI and environment variables.
2738        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
2739        let frozen = resolve_frozen(frozen, no_frozen, FrozenFlag::Frozen, environment.frozen);
2740
2741        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
2742
2743        let (dev, no_dev) = resolve_flag_pair(
2744            dev,
2745            no_dev,
2746            "dev",
2747            "no-dev",
2748            Some(environment.dev),
2749            Some(environment.no_dev),
2750        );
2751
2752        Ok(Self {
2753            groups: DependencyGroups::from_args(
2754                DevMode::from_args(dev.into(), no_dev.into(), only_dev),
2755                group,
2756                if no_group.is_empty() {
2757                    environment.no_group.clone().unwrap_or_default()
2758                } else {
2759                    no_group
2760                },
2761                no_default_groups,
2762                only_group,
2763                all_groups,
2764            ),
2765            lock_check: locked,
2766            frozen,
2767            universal,
2768            format,
2769            depth: tree.depth,
2770            prune: tree.prune,
2771            package: tree.package,
2772            no_dedupe: tree.no_dedupe,
2773            invert: tree.invert,
2774            outdated: tree.outdated,
2775            show_sizes: tree.show_sizes,
2776            script,
2777            python_version,
2778            python_platform,
2779            python: python.and_then(Maybe::into_option),
2780            resolver: resolve_resolver_settings(resolver, build, filesystem, &environment)?,
2781            install_mirrors: environment
2782                .install_mirrors
2783                .combine(filesystem_install_mirrors),
2784        })
2785    }
2786}
2787
2788/// The resolved settings to use for an `export` invocation.
2789#[expect(clippy::struct_excessive_bools)]
2790#[derive(Debug, Clone)]
2791pub struct ExportSettings {
2792    pub format: Option<ExportFormat>,
2793    pub all_packages: bool,
2794    pub package: Vec<PackageName>,
2795    pub prune: Vec<PackageName>,
2796    pub extras: ExtrasSpecification,
2797    pub groups: DependencyGroups,
2798    pub editable: Option<EditableMode>,
2799    pub hashes: bool,
2800    pub install_options: InstallOptions,
2801    pub batch: Option<PathBuf>,
2802    pub output_file: Option<PathBuf>,
2803    pub lock_check: LockCheck,
2804    pub frozen: Option<FrozenSource>,
2805    pub include_annotations: bool,
2806    pub include_header: bool,
2807    pub include_index_url: bool,
2808    pub include_find_links: bool,
2809    pub script: Option<PathBuf>,
2810    pub python: Option<String>,
2811    pub install_mirrors: PythonInstallMirrors,
2812    pub refresh: Refresh,
2813    pub settings: ResolverSettings,
2814}
2815
2816impl ExportSettings {
2817    /// Resolve the [`ExportSettings`] from the CLI and filesystem configuration.
2818    pub fn resolve(
2819        args: ExportArgs,
2820        filesystem: Option<FilesystemOptions>,
2821        environment: EnvironmentOptions,
2822    ) -> anyhow::Result<Self> {
2823        let ExportArgs {
2824            format,
2825            all_packages,
2826            package,
2827            prune,
2828            extra,
2829            all_extras,
2830            no_extra,
2831            no_all_extras,
2832            dependency_groups:
2833                ProjectDependencyGroupsArgs {
2834                    dev,
2835                    no_dev,
2836                    only_dev,
2837                    group,
2838                    no_group,
2839                    no_default_groups,
2840                    only_group,
2841                    all_groups,
2842                },
2843            annotate,
2844            no_annotate,
2845            header,
2846            no_header,
2847            emit_index_url,
2848            no_emit_index_url,
2849            emit_find_links,
2850            no_emit_find_links,
2851            editable,
2852            no_editable,
2853            no_editable_package,
2854            hashes,
2855            no_hashes,
2856            batch,
2857            output_file,
2858            no_emit_project,
2859            only_emit_project,
2860            no_emit_workspace,
2861            only_emit_workspace,
2862            no_emit_local,
2863            only_emit_local,
2864            no_emit_package,
2865            only_emit_package,
2866            locked,
2867            no_locked,
2868            frozen: frozen_cli,
2869            no_frozen,
2870            resolver,
2871            build,
2872            refresh,
2873            script,
2874            python,
2875        } = args;
2876        let filesystem_install_mirrors = filesystem
2877            .as_ref()
2878            .map(|fs| fs.install_mirrors.clone())
2879            .unwrap_or_default();
2880
2881        // Resolve flags from CLI and environment variables.
2882        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
2883        let frozen = resolve_frozen(
2884            frozen_cli,
2885            no_frozen,
2886            FrozenFlag::Frozen,
2887            environment.frozen,
2888        );
2889
2890        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
2891
2892        let (dev, no_dev) = resolve_flag_pair(
2893            dev,
2894            no_dev,
2895            "dev",
2896            "no-dev",
2897            Some(environment.dev),
2898            Some(environment.no_dev),
2899        );
2900        let (editable, no_editable) = resolve_flag_pair(
2901            editable,
2902            no_editable,
2903            "editable",
2904            "no-editable",
2905            None,
2906            Some(environment.no_editable),
2907        );
2908
2909        Ok(Self {
2910            format,
2911            all_packages,
2912            package,
2913            prune,
2914            extras: ExtrasSpecification::from_args(
2915                extra.unwrap_or_default(),
2916                no_extra,
2917                // TODO(blueraft): support no_default_extras
2918                false,
2919                // TODO(blueraft): support only_extra
2920                vec![],
2921                flag(all_extras, no_all_extras, "all-extras")?.unwrap_or_default(),
2922            ),
2923            groups: DependencyGroups::from_args(
2924                DevMode::from_args(dev.into(), no_dev.into(), only_dev),
2925                group,
2926                if no_group.is_empty() {
2927                    environment.no_group.clone().unwrap_or_default()
2928                } else {
2929                    no_group
2930                },
2931                no_default_groups,
2932                only_group,
2933                all_groups,
2934            ),
2935            editable: EditableMode::from_args(
2936                flag(editable.into(), no_editable.into(), "editable")?,
2937                no_editable_package,
2938            ),
2939            hashes: flag(hashes, no_hashes, "hashes")?.unwrap_or(true),
2940            install_options: InstallOptions::new(
2941                no_emit_project,
2942                only_emit_project,
2943                no_emit_workspace,
2944                only_emit_workspace,
2945                no_emit_local,
2946                only_emit_local,
2947                no_emit_package,
2948                only_emit_package,
2949            ),
2950            batch,
2951            output_file,
2952            lock_check: locked,
2953            frozen,
2954            include_annotations: flag(annotate, no_annotate, "annotate")?.unwrap_or(true),
2955            include_header: flag(header, no_header, "header")?.unwrap_or(true),
2956            include_index_url: flag(emit_index_url, no_emit_index_url, "emit-index-url")?
2957                .unwrap_or(false),
2958            include_find_links: flag(emit_find_links, no_emit_find_links, "emit-find-links")?
2959                .unwrap_or(false),
2960            script,
2961            python: python.and_then(Maybe::into_option),
2962            refresh: Refresh::try_from(refresh)?,
2963            settings: resolve_resolver_settings(resolver, build, filesystem, &environment)?,
2964            install_mirrors: environment
2965                .install_mirrors
2966                .combine(filesystem_install_mirrors),
2967        })
2968    }
2969}
2970
2971/// The resolved settings to use for a `format` invocation.
2972#[derive(Debug, Clone)]
2973pub struct FormatSettings {
2974    pub ruff_path: Option<PathBuf>,
2975    pub check: bool,
2976    pub diff: bool,
2977    pub extra_args: Vec<String>,
2978    pub version: Option<String>,
2979    pub exclude_newer: Option<jiff::Timestamp>,
2980    pub no_project: bool,
2981    pub show_version: bool,
2982}
2983
2984impl FormatSettings {
2985    /// Resolve the [`FormatSettings`] from the CLI and filesystem configuration.
2986    pub fn resolve(
2987        args: FormatArgs,
2988        _filesystem: Option<FilesystemOptions>,
2989        environment: EnvironmentOptions,
2990    ) -> Self {
2991        let FormatArgs {
2992            check,
2993            diff,
2994            extra_args,
2995            version,
2996            exclude_newer,
2997            no_project,
2998            show_version,
2999        } = args;
3000
3001        Self {
3002            ruff_path: environment.ruff_path,
3003            check,
3004            diff,
3005            extra_args,
3006            version,
3007            exclude_newer: exclude_newer
3008                .and_then(ExcludeNewerOverride::into_value)
3009                .map(|value| value.timestamp()),
3010            no_project,
3011            show_version,
3012        }
3013    }
3014}
3015
3016/// The resolved settings to use for a `check` invocation.
3017#[derive(Debug, Clone)]
3018pub struct CheckSettings {
3019    pub ty_path: Option<PathBuf>,
3020    #[expect(dead_code)]
3021    script: Option<PathBuf>,
3022    pub fix: bool,
3023    pub all_packages: bool,
3024    pub package: Vec<PackageName>,
3025    pub extras: ExtrasSpecification,
3026    pub groups: DependencyGroups,
3027    pub lock_check: LockCheck,
3028    pub frozen: Option<FrozenSource>,
3029    pub no_sync: bool,
3030    pub no_install_project: bool,
3031    pub isolated: bool,
3032    pub python: Option<String>,
3033    pub install_mirrors: PythonInstallMirrors,
3034    pub refresh: Refresh,
3035    pub settings: ResolverInstallerSettings,
3036    pub ty_version: Option<String>,
3037    pub show_version: bool,
3038    pub show_command: bool,
3039    pub no_project: bool,
3040    pub malware_settings: MalwareCheckSettings,
3041}
3042
3043impl CheckSettings {
3044    /// Resolve the [`CheckSettings`] from the CLI and filesystem configuration.
3045    pub fn resolve(
3046        args: CheckArgs,
3047        filesystem: Option<FilesystemOptions>,
3048        environment: EnvironmentOptions,
3049    ) -> anyhow::Result<Self> {
3050        let CheckArgs {
3051            fix,
3052            all_packages,
3053            package,
3054            script,
3055            extra,
3056            all_extras,
3057            no_extra,
3058            no_all_extras,
3059            dependency_groups:
3060                ProjectDependencyGroupsArgs {
3061                    dev,
3062                    no_dev,
3063                    only_dev,
3064                    group,
3065                    no_group,
3066                    no_default_groups,
3067                    only_group,
3068                    all_groups,
3069                },
3070            locked,
3071            no_locked,
3072            frozen,
3073            no_frozen,
3074            no_sync,
3075            no_install_project,
3076            isolated,
3077            python,
3078            ty_version,
3079            show_version,
3080            show_command,
3081            no_project,
3082            installer,
3083            build,
3084            refresh,
3085        } = args;
3086
3087        let filesystem_install_mirrors = filesystem
3088            .as_ref()
3089            .map(|fs| fs.install_mirrors.clone())
3090            .unwrap_or_default();
3091
3092        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
3093        let frozen = resolve_frozen(frozen, no_frozen, FrozenFlag::Frozen, environment.frozen);
3094        let no_sync = resolve_flag(no_sync, "no-sync", environment.no_sync);
3095        let no_install_project = resolve_flag(
3096            no_install_project,
3097            "no-install-project",
3098            environment.no_install_project,
3099        );
3100        let isolated = resolve_flag(isolated, "isolated", environment.isolated).is_enabled();
3101        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
3102        check_conflicts(no_install_project, no_sync)?;
3103        if script.is_some() {
3104            check_conflicts(no_install_project, Flag::from_cli("script"))?;
3105        }
3106        if no_project {
3107            check_conflicts(no_install_project, Flag::from_cli("no-project"))?;
3108        }
3109
3110        let (dev, no_dev) = resolve_flag_pair(
3111            dev,
3112            no_dev,
3113            "dev",
3114            "no-dev",
3115            Some(environment.dev),
3116            Some(environment.no_dev),
3117        );
3118        let malware_settings = MalwareCheckSettings::resolve(filesystem.as_ref(), &environment);
3119        let settings =
3120            resolve_resolver_installer_settings(installer, build, filesystem, &environment)?;
3121        Ok(Self {
3122            ty_path: environment.ty_path,
3123            script,
3124            fix,
3125            all_packages,
3126            package,
3127            extras: ExtrasSpecification::from_args(
3128                extra.unwrap_or_default(),
3129                no_extra,
3130                false,
3131                vec![],
3132                flag(all_extras, no_all_extras, "all-extras")?.unwrap_or_default(),
3133            ),
3134            groups: DependencyGroups::from_args(
3135                DevMode::from_args(dev.into(), no_dev.into(), only_dev),
3136                group,
3137                if no_group.is_empty() {
3138                    environment.no_group.clone().unwrap_or_default()
3139                } else {
3140                    no_group
3141                },
3142                no_default_groups,
3143                only_group,
3144                all_groups,
3145            ),
3146            lock_check: locked,
3147            frozen,
3148            no_sync: no_sync.is_enabled(),
3149            no_install_project: no_install_project.is_enabled(),
3150            isolated,
3151            python: python.and_then(Maybe::into_option),
3152            install_mirrors: environment
3153                .install_mirrors
3154                .combine(filesystem_install_mirrors),
3155            refresh: Refresh::try_from(refresh)?,
3156            settings,
3157            ty_version,
3158            show_version,
3159            show_command,
3160            no_project,
3161            malware_settings,
3162        })
3163    }
3164}
3165
3166/// The resolved settings to use for an `audit` invocation.
3167#[derive(Debug, Clone)]
3168pub struct AuditSettings {
3169    pub extras: ExtrasSpecification,
3170    pub groups: DependencyGroups,
3171    pub lock_check: LockCheck,
3172    pub frozen: Option<FrozenSource>,
3173    pub python_version: Option<PythonVersion>,
3174    pub python_platform: Option<TargetTriple>,
3175    pub install_mirrors: PythonInstallMirrors,
3176    pub settings: ResolverSettings,
3177    pub output_format: AuditOutputFormat,
3178    pub service_format: VulnerabilityServiceFormat,
3179    pub service_url: Option<SafeUrl>,
3180    pub ignore: Vec<VulnerabilityID>,
3181    pub ignore_until_fixed: Vec<VulnerabilityID>,
3182}
3183
3184impl AuditSettings {
3185    /// Resolve the [`AuditSettings`] from the CLI and filesystem configuration.
3186    pub fn resolve(
3187        args: AuditArgs,
3188        filesystem: Option<FilesystemOptions>,
3189        environment: EnvironmentOptions,
3190    ) -> anyhow::Result<Self> {
3191        let AuditArgs {
3192            no_extra,
3193            no_dev,
3194            no_group,
3195            no_default_groups,
3196            only_group,
3197            only_dev,
3198            script: _,
3199            python_version,
3200            python_platform,
3201            locked,
3202            no_locked,
3203            frozen,
3204            no_frozen,
3205            audit:
3206                AuditCommonArgs {
3207                    offline: _,
3208                    output_format,
3209                    ignore,
3210                    ignore_until_fixed,
3211                    service_format,
3212                    service_url,
3213                },
3214            build,
3215            resolver,
3216        } = args;
3217
3218        let filesystem_install_mirrors = filesystem
3219            .as_ref()
3220            .map(|fs| fs.install_mirrors.clone())
3221            .unwrap_or_default();
3222
3223        let filesystem_audit = filesystem
3224            .as_ref()
3225            .and_then(|fs| fs.audit.clone())
3226            .unwrap_or_default();
3227
3228        let no_dev = no_dev || environment.no_dev.value == Some(true);
3229
3230        // Resolve flags from CLI and environment variables.
3231        let locked = resolve_lock_check(locked, no_locked, LockedFlag::Locked, environment.locked);
3232        let frozen = resolve_frozen(frozen, no_frozen, FrozenFlag::Frozen, environment.frozen);
3233
3234        let (locked, frozen) = resolve_lock_flags(locked, frozen)?;
3235
3236        // Audit includes all groups by default, regardless of `tool.uv.default-groups`.
3237        // `--no-default-groups` disables that implicit selection.
3238        let all_groups = only_group.is_empty() && !only_dev && !no_default_groups;
3239
3240        Ok(Self {
3241            extras: ExtrasSpecification::from_args(
3242                vec![],
3243                no_extra,
3244                // TODO(ww): support no_default_extras?
3245                false,
3246                // TODO(ww): support only_extra?
3247                vec![],
3248                true,
3249            ),
3250            groups: DependencyGroups::from_args(
3251                DevMode::from_args(all_groups, no_dev, only_dev),
3252                vec![],
3253                if no_group.is_empty() {
3254                    environment.no_group.clone().unwrap_or_default()
3255                } else {
3256                    no_group
3257                },
3258                no_default_groups,
3259                only_group,
3260                all_groups,
3261            ),
3262            lock_check: locked,
3263            frozen,
3264            python_version,
3265            python_platform,
3266            settings: resolve_resolver_settings(resolver, build, filesystem, &environment)?,
3267            install_mirrors: environment
3268                .install_mirrors
3269                .combine(filesystem_install_mirrors),
3270            output_format,
3271            service_format,
3272            service_url,
3273            ignore: {
3274                let config_ignore = filesystem_audit.ignore.unwrap_or_default();
3275                let mut merged = ignore;
3276                merged.extend(config_ignore);
3277                merged.into_iter().map(VulnerabilityID::new).collect()
3278            },
3279            ignore_until_fixed: {
3280                let config_ignore_until_fixed =
3281                    filesystem_audit.ignore_until_fixed.unwrap_or_default();
3282                let mut merged = ignore_until_fixed;
3283                merged.extend(config_ignore_until_fixed);
3284                merged.into_iter().map(VulnerabilityID::new).collect()
3285            },
3286        })
3287    }
3288}
3289
3290fn workspace_overrides(filesystem: Option<&FilesystemOptions>) -> Vec<Override<Requirement>> {
3291    let mut overrides = Vec::new();
3292    for dependency in filesystem
3293        .and_then(|configuration| configuration.override_dependencies.as_ref())
3294        .into_iter()
3295        .flatten()
3296    {
3297        match dependency {
3298            OverrideDependency::Requirement(requirement) => {
3299                overrides.push(Override::Requirement(Requirement::from(
3300                    requirement
3301                        .clone()
3302                        .with_origin(RequirementOrigin::Workspace),
3303                )));
3304            }
3305            OverrideDependency::Package(package) => {
3306                overrides.push(Override::Package(PackageOverride {
3307                    package: package.package.clone(),
3308                    dependencies: package
3309                        .dependencies
3310                        .iter()
3311                        .cloned()
3312                        .map(|requirement| {
3313                            Requirement::from(requirement.with_origin(RequirementOrigin::Workspace))
3314                        })
3315                        .collect(),
3316                }));
3317            }
3318        }
3319    }
3320    overrides
3321}
3322
3323/// Resolve requirements-file arguments, using environment values only when the CLI did not
3324/// provide the corresponding option.
3325fn requirement_files(
3326    arguments: Vec<Maybe<RequirementsInput>>,
3327    environment: Option<Vec<RequirementsInput>>,
3328) -> Vec<RequirementsInput> {
3329    if arguments.is_empty() {
3330        environment.unwrap_or_default()
3331    } else {
3332        arguments
3333            .into_iter()
3334            .filter_map(Maybe::into_option)
3335            .collect()
3336    }
3337}
3338
3339/// The resolved settings to use for a `pip compile` invocation.
3340#[derive(Debug, Clone)]
3341pub struct PipCompileSettings {
3342    pub format: Option<PipCompileFormat>,
3343    pub src_file: Vec<RequirementsInput>,
3344    pub constraints: Vec<RequirementsInput>,
3345    pub overrides: Vec<RequirementsInput>,
3346    pub excludes: Vec<RequirementsInput>,
3347    pub build_constraints: Vec<RequirementsInput>,
3348    pub build_hash_checking: HashCheckingMode,
3349    pub constraints_from_workspace: Vec<Requirement>,
3350    pub overrides_from_workspace: Vec<Override<Requirement>>,
3351    pub excludes_from_workspace: Vec<ExcludeDependency>,
3352    pub build_constraints_from_workspace: Vec<NameRequirementSpecification>,
3353    pub environments: SupportedEnvironments,
3354    pub required_environments: SupportedEnvironments,
3355    pub minimum_libc_version: Option<MinimumLibcVersion>,
3356    pub refresh: Refresh,
3357    pub settings: PipSettings,
3358}
3359
3360impl PipCompileSettings {
3361    /// Resolve the [`PipCompileSettings`] from the CLI and filesystem configuration.
3362    pub fn resolve(
3363        args: PipCompileArgs,
3364        filesystem: Option<FilesystemOptions>,
3365        environment: EnvironmentOptions,
3366    ) -> anyhow::Result<Self> {
3367        let PipCompileArgs {
3368            src_file,
3369            constraints:
3370                DependencyConstraintsArgs {
3371                    constraints,
3372                    overrides,
3373                    excludes,
3374                    build_constraints,
3375                },
3376            extra,
3377            all_extras,
3378            no_all_extras,
3379            require_build_hashes,
3380            no_require_build_hashes,
3381            refresh,
3382            no_deps,
3383            deps,
3384            group,
3385            output_file,
3386            format,
3387            no_strip_extras,
3388            strip_extras,
3389            no_strip_markers,
3390            strip_markers,
3391            no_annotate,
3392            annotate,
3393            no_header,
3394            header,
3395            annotation_style,
3396            custom_compile_command,
3397            resolver,
3398            python,
3399            system,
3400            no_system,
3401            generate_hashes,
3402            no_generate_hashes,
3403            no_build,
3404            build,
3405            no_binary,
3406            only_binary,
3407            python_version,
3408            python_platform,
3409            universal,
3410            no_universal,
3411            no_emit_package,
3412            emit_index_url,
3413            no_emit_index_url,
3414            emit_find_links,
3415            no_emit_find_links,
3416            emit_build_options,
3417            no_emit_build_options,
3418            emit_marker_expression,
3419            no_emit_marker_expression,
3420            emit_index_annotation,
3421            no_emit_index_annotation,
3422            torch_backend,
3423            compat_args: _,
3424        } = args;
3425
3426        let constraints_from_workspace = if let Some(configuration) = &filesystem {
3427            configuration
3428                .constraint_dependencies
3429                .clone()
3430                .unwrap_or_default()
3431                .into_iter()
3432                .map(|requirement| {
3433                    Requirement::from(requirement.with_origin(RequirementOrigin::Workspace))
3434                })
3435                .collect()
3436        } else {
3437            Vec::new()
3438        };
3439
3440        let overrides_from_workspace = workspace_overrides(filesystem.as_ref());
3441
3442        let excludes_from_workspace = if let Some(configuration) = &filesystem {
3443            configuration
3444                .exclude_dependencies
3445                .clone()
3446                .unwrap_or_default()
3447        } else {
3448            Vec::new()
3449        };
3450
3451        let build_constraints_from_workspace = if let Some(configuration) = &filesystem {
3452            configuration
3453                .build_constraint_dependencies
3454                .clone()
3455                .unwrap_or_default()
3456                .into_iter()
3457                .map(|requirement| {
3458                    let (requirement, hashes) = requirement.into_parts();
3459                    NameRequirementSpecification {
3460                        requirement: Requirement::from(
3461                            requirement.with_origin(RequirementOrigin::Workspace),
3462                        ),
3463                        hashes,
3464                    }
3465                })
3466                .collect()
3467        } else {
3468            Vec::new()
3469        };
3470
3471        let environments = if let Some(configuration) = &filesystem {
3472            configuration.environments.clone().unwrap_or_default()
3473        } else {
3474            SupportedEnvironments::default()
3475        };
3476
3477        let required_environments = if let Some(configuration) = &filesystem {
3478            configuration
3479                .required_environments
3480                .clone()
3481                .unwrap_or_default()
3482        } else {
3483            SupportedEnvironments::default()
3484        };
3485
3486        let minimum_libc_version = filesystem
3487            .as_ref()
3488            .and_then(|configuration| configuration.minimum_libc_version);
3489
3490        Ok(Self {
3491            format,
3492            src_file,
3493            constraints: requirement_files(constraints, environment.constraints.clone()),
3494            build_constraints: requirement_files(
3495                build_constraints,
3496                environment.build_constraints.clone(),
3497            ),
3498            build_hash_checking: resolve_pip_build_hash_checking(
3499                flag(
3500                    require_build_hashes,
3501                    no_require_build_hashes,
3502                    "require-build-hashes",
3503                )?,
3504                filesystem.as_ref(),
3505                &environment,
3506            ),
3507            overrides: requirement_files(overrides, environment.overrides.clone()),
3508            excludes: requirement_files(excludes, environment.excludes.clone()),
3509            constraints_from_workspace,
3510            overrides_from_workspace,
3511            excludes_from_workspace,
3512            build_constraints_from_workspace,
3513            environments,
3514            required_environments,
3515            minimum_libc_version,
3516            refresh: Refresh::try_from(refresh)?,
3517            settings: PipSettings::combine(
3518                PipOptions {
3519                    python: python.and_then(Maybe::into_option),
3520                    system: flag(system, no_system, "system")?,
3521                    no_build: flag(no_build, build, "build")?,
3522                    no_binary,
3523                    only_binary,
3524                    extra,
3525                    all_extras: flag(all_extras, no_all_extras, "all-extras")?,
3526                    no_deps: flag(no_deps, deps, "deps")?,
3527                    group: Some(group),
3528                    output_file,
3529                    no_strip_extras: flag(no_strip_extras, strip_extras, "strip-extras")?,
3530                    no_strip_markers: flag(no_strip_markers, strip_markers, "strip-markers")?,
3531                    no_annotate: flag(no_annotate, annotate, "annotate")?,
3532                    no_header: flag(no_header, header, "header")?,
3533                    custom_compile_command,
3534                    generate_hashes: flag(generate_hashes, no_generate_hashes, "generate-hashes")?,
3535                    python_version,
3536                    python_platform,
3537                    universal: flag(universal, no_universal, "universal")?,
3538                    no_emit_package,
3539                    emit_index_url: flag(emit_index_url, no_emit_index_url, "emit-index-url")?,
3540                    emit_find_links: flag(emit_find_links, no_emit_find_links, "emit-find-links")?,
3541                    emit_build_options: flag(
3542                        emit_build_options,
3543                        no_emit_build_options,
3544                        "emit-build-options",
3545                    )?,
3546                    emit_marker_expression: flag(
3547                        emit_marker_expression,
3548                        no_emit_marker_expression,
3549                        "emit-marker-expression",
3550                    )?,
3551                    emit_index_annotation: flag(
3552                        emit_index_annotation,
3553                        no_emit_index_annotation,
3554                        "emit-index-annotation",
3555                    )?,
3556                    annotation_style,
3557                    torch_backend,
3558                    ..resolver.into_pip_options(configured_indexes(filesystem.as_ref()))?
3559                },
3560                filesystem,
3561                environment,
3562            ),
3563        })
3564    }
3565}
3566
3567/// The resolved settings to use for a `pip sync` invocation.
3568#[derive(Debug, Clone)]
3569pub struct PipSyncSettings {
3570    pub src_file: Vec<RequirementsInput>,
3571    pub constraints: Vec<RequirementsInput>,
3572    pub build_constraints: Vec<RequirementsInput>,
3573    pub build_hash_checking: HashCheckingMode,
3574    pub dry_run: DryRun,
3575    pub output_format: PipInstallFormat,
3576    pub refresh: Refresh,
3577    pub settings: PipSettings,
3578}
3579
3580impl PipSyncSettings {
3581    /// Resolve the [`PipSyncSettings`] from the CLI and filesystem configuration.
3582    pub fn resolve(
3583        args: Box<PipSyncArgs>,
3584        filesystem: Option<FilesystemOptions>,
3585        environment: EnvironmentOptions,
3586    ) -> anyhow::Result<Self> {
3587        let PipSyncArgs {
3588            src_file,
3589            constraints,
3590            build_constraints,
3591            require_build_hashes,
3592            no_require_build_hashes,
3593            extra,
3594            all_extras,
3595            no_all_extras,
3596            group,
3597            installer,
3598            refresh,
3599            hash_checking:
3600                HashCheckingArgs {
3601                    require_hashes,
3602                    no_require_hashes,
3603                    verify_hashes,
3604                    no_verify_hashes,
3605                },
3606            python,
3607            system,
3608            no_system,
3609            break_system_packages,
3610            no_break_system_packages,
3611            target,
3612            prefix,
3613            allow_empty_requirements,
3614            no_allow_empty_requirements,
3615            no_build,
3616            build,
3617            no_binary,
3618            only_binary,
3619            python_version,
3620            python_platform,
3621            strict,
3622            no_strict,
3623            dry_run,
3624            output_format,
3625            torch_backend,
3626            compat_args: _,
3627            check,
3628        } = *args;
3629
3630        Ok(Self {
3631            src_file,
3632            constraints: requirement_files(constraints, environment.constraints.clone()),
3633            build_constraints: requirement_files(
3634                build_constraints,
3635                environment.build_constraints.clone(),
3636            ),
3637            build_hash_checking: resolve_pip_build_hash_checking(
3638                flag(
3639                    require_build_hashes,
3640                    no_require_build_hashes,
3641                    "require-build-hashes",
3642                )?,
3643                filesystem.as_ref(),
3644                &environment,
3645            ),
3646            dry_run: if check {
3647                DryRun::Check
3648            } else {
3649                DryRun::from_args(dry_run)
3650            },
3651            output_format,
3652            refresh: Refresh::try_from(refresh)?,
3653            settings: PipSettings::combine(
3654                PipOptions {
3655                    python: python.and_then(Maybe::into_option),
3656                    system: flag(system, no_system, "system")?,
3657                    break_system_packages: flag(
3658                        break_system_packages,
3659                        no_break_system_packages,
3660                        "break-system-packages",
3661                    )?,
3662                    target,
3663                    prefix,
3664                    require_hashes: flag(require_hashes, no_require_hashes, "require-hashes")?,
3665                    verify_hashes: flag(verify_hashes, no_verify_hashes, "verify-hashes")?,
3666                    no_build: flag(no_build, build, "build")?,
3667                    no_binary,
3668                    only_binary,
3669                    allow_empty_requirements: flag(
3670                        allow_empty_requirements,
3671                        no_allow_empty_requirements,
3672                        "allow-empty-requirements",
3673                    )?,
3674                    python_version,
3675                    python_platform,
3676                    strict: flag(strict, no_strict, "strict")?,
3677                    extra,
3678                    all_extras: flag(all_extras, no_all_extras, "all-extras")?,
3679                    group: Some(group),
3680                    torch_backend,
3681                    ..installer.into_pip_options(configured_indexes(filesystem.as_ref()))?
3682                },
3683                filesystem,
3684                environment,
3685            ),
3686        })
3687    }
3688}
3689
3690/// The resolved settings to use for a `pip install` invocation.
3691#[derive(Debug, Clone)]
3692pub struct PipInstallSettings {
3693    pub package: Vec<String>,
3694    pub requirements: Vec<RequirementsInput>,
3695    pub editables: Vec<String>,
3696    pub editable: Option<EditableMode>,
3697    pub constraints: Vec<RequirementsInput>,
3698    pub overrides: Vec<RequirementsInput>,
3699    pub excludes: Vec<RequirementsInput>,
3700    pub build_constraints: Vec<RequirementsInput>,
3701    pub build_hash_checking: HashCheckingMode,
3702    pub dry_run: DryRun,
3703    pub output_format: PipInstallFormat,
3704    pub constraints_from_workspace: Vec<Requirement>,
3705    pub overrides_from_workspace: Vec<Override<Requirement>>,
3706    pub excludes_from_workspace: Vec<ExcludeDependency>,
3707    pub build_constraints_from_workspace: Vec<NameRequirementSpecification>,
3708    pub modifications: Modifications,
3709    pub refresh: Refresh,
3710    pub settings: PipSettings,
3711}
3712
3713impl PipInstallSettings {
3714    /// Resolve the [`PipInstallSettings`] from the CLI and filesystem configuration.
3715    pub fn resolve(
3716        args: PipInstallArgs,
3717        filesystem: Option<FilesystemOptions>,
3718        environment: EnvironmentOptions,
3719    ) -> anyhow::Result<Self> {
3720        let PipInstallArgs {
3721            package,
3722            requirements,
3723            editable,
3724            no_editable,
3725            no_editable_package,
3726            constraints:
3727                DependencyConstraintsArgs {
3728                    constraints,
3729                    overrides,
3730                    excludes,
3731                    build_constraints,
3732                },
3733            require_build_hashes,
3734            no_require_build_hashes,
3735            extra,
3736            all_extras,
3737            no_all_extras,
3738            installer,
3739            refresh,
3740            no_deps,
3741            deps,
3742            group,
3743            hash_checking:
3744                HashCheckingArgs {
3745                    require_hashes,
3746                    no_require_hashes,
3747                    verify_hashes,
3748                    no_verify_hashes,
3749                },
3750            python,
3751            system,
3752            no_system,
3753            break_system_packages,
3754            no_break_system_packages,
3755            target,
3756            prefix,
3757            no_build,
3758            build,
3759            no_binary,
3760            only_binary,
3761            python_version,
3762            python_platform,
3763            inexact,
3764            exact,
3765            strict,
3766            no_strict,
3767            dry_run,
3768            output_format,
3769            torch_backend,
3770            compat_args: _,
3771            check,
3772        } = args;
3773
3774        let constraints_from_workspace = if let Some(configuration) = &filesystem {
3775            configuration
3776                .constraint_dependencies
3777                .clone()
3778                .unwrap_or_default()
3779                .into_iter()
3780                .map(|requirement| {
3781                    Requirement::from(requirement.with_origin(RequirementOrigin::Workspace))
3782                })
3783                .collect()
3784        } else {
3785            Vec::new()
3786        };
3787
3788        let overrides_from_workspace = workspace_overrides(filesystem.as_ref());
3789
3790        let excludes_from_workspace = if let Some(configuration) = &filesystem {
3791            configuration
3792                .exclude_dependencies
3793                .clone()
3794                .unwrap_or_default()
3795        } else {
3796            Vec::new()
3797        };
3798
3799        let build_constraints_from_workspace = if let Some(configuration) = &filesystem {
3800            configuration
3801                .build_constraint_dependencies
3802                .clone()
3803                .unwrap_or_default()
3804                .into_iter()
3805                .map(|requirement| {
3806                    let (requirement, hashes) = requirement.into_parts();
3807                    NameRequirementSpecification {
3808                        requirement: Requirement::from(
3809                            requirement.with_origin(RequirementOrigin::Workspace),
3810                        ),
3811                        hashes,
3812                    }
3813                })
3814                .collect()
3815        } else {
3816            Vec::new()
3817        };
3818
3819        Ok(Self {
3820            package,
3821            requirements,
3822            editables: editable,
3823            constraints: requirement_files(constraints, environment.constraints.clone()),
3824            overrides: requirement_files(overrides, environment.overrides.clone()),
3825            excludes: requirement_files(excludes, environment.excludes.clone()),
3826            build_constraints: requirement_files(
3827                build_constraints,
3828                environment.build_constraints.clone(),
3829            ),
3830            build_hash_checking: resolve_pip_build_hash_checking(
3831                flag(
3832                    require_build_hashes,
3833                    no_require_build_hashes,
3834                    "require-build-hashes",
3835                )?,
3836                filesystem.as_ref(),
3837                &environment,
3838            ),
3839            dry_run: if check {
3840                DryRun::Check
3841            } else {
3842                DryRun::from_args(dry_run)
3843            },
3844            output_format,
3845            constraints_from_workspace,
3846            overrides_from_workspace,
3847            excludes_from_workspace,
3848            build_constraints_from_workspace,
3849            modifications: if flag(exact, inexact, "inexact")?.unwrap_or(false) {
3850                Modifications::Exact
3851            } else {
3852                Modifications::Sufficient
3853            },
3854            editable: EditableMode::from_args(
3855                if no_editable || environment.no_editable.value == Some(true) {
3856                    Some(false)
3857                } else {
3858                    None
3859                },
3860                no_editable_package,
3861            ),
3862            refresh: Refresh::try_from(refresh)?,
3863            settings: PipSettings::combine(
3864                PipOptions {
3865                    python: python.and_then(Maybe::into_option),
3866                    system: flag(system, no_system, "system")?,
3867                    break_system_packages: flag(
3868                        break_system_packages,
3869                        no_break_system_packages,
3870                        "break-system-packages",
3871                    )?,
3872                    target,
3873                    prefix,
3874                    no_build: flag(no_build, build, "build")?,
3875                    no_binary,
3876                    only_binary,
3877                    strict: flag(strict, no_strict, "strict")?,
3878                    extra,
3879                    all_extras: flag(all_extras, no_all_extras, "all-extras")?,
3880                    group: Some(group),
3881                    no_deps: flag(no_deps, deps, "deps")?,
3882                    python_version,
3883                    python_platform,
3884                    require_hashes: flag(require_hashes, no_require_hashes, "require-hashes")?,
3885                    verify_hashes: flag(verify_hashes, no_verify_hashes, "verify-hashes")?,
3886                    torch_backend,
3887                    ..installer.into_pip_options(configured_indexes(filesystem.as_ref()))?
3888                },
3889                filesystem,
3890                environment,
3891            ),
3892        })
3893    }
3894}
3895
3896/// The resolved settings to use for a `pip uninstall` invocation.
3897#[derive(Debug, Clone)]
3898pub struct PipUninstallSettings {
3899    pub package: Vec<String>,
3900    pub requirements: Vec<RequirementsInput>,
3901    pub dry_run: DryRun,
3902    pub settings: PipSettings,
3903}
3904
3905impl PipUninstallSettings {
3906    /// Resolve the [`PipUninstallSettings`] from the CLI and filesystem configuration.
3907    pub fn resolve(
3908        args: PipUninstallArgs,
3909        filesystem: Option<FilesystemOptions>,
3910        environment: EnvironmentOptions,
3911    ) -> anyhow::Result<Self> {
3912        let PipUninstallArgs {
3913            package,
3914            requirements,
3915            python,
3916            keyring_provider,
3917            system,
3918            no_system,
3919            break_system_packages,
3920            no_break_system_packages,
3921            target,
3922            prefix,
3923            dry_run,
3924            compat_args: _,
3925        } = args;
3926
3927        Ok(Self {
3928            package,
3929            requirements,
3930            dry_run: DryRun::from_args(dry_run),
3931            settings: PipSettings::combine(
3932                PipOptions {
3933                    python: python.and_then(Maybe::into_option),
3934                    system: flag(system, no_system, "system")?,
3935                    break_system_packages: flag(
3936                        break_system_packages,
3937                        no_break_system_packages,
3938                        "break-system-packages",
3939                    )?,
3940                    target,
3941                    prefix,
3942                    keyring_provider,
3943                    ..PipOptions::default()
3944                },
3945                filesystem,
3946                environment,
3947            ),
3948        })
3949    }
3950}
3951
3952/// The resolved settings to use for a `pip freeze` invocation.
3953#[derive(Debug, Clone)]
3954pub struct PipFreezeSettings {
3955    pub exclude_editable: bool,
3956    pub exclude: FxHashSet<PackageName>,
3957    pub paths: Option<Vec<PathBuf>>,
3958    pub settings: PipSettings,
3959}
3960
3961impl PipFreezeSettings {
3962    /// Resolve the [`PipFreezeSettings`] from the CLI and filesystem configuration.
3963    pub fn resolve(
3964        args: PipFreezeArgs,
3965        filesystem: Option<FilesystemOptions>,
3966        environment: EnvironmentOptions,
3967    ) -> anyhow::Result<Self> {
3968        let PipFreezeArgs {
3969            exclude_editable,
3970            exclude,
3971            strict,
3972            no_strict,
3973            python,
3974            paths,
3975            system,
3976            no_system,
3977            target,
3978            prefix,
3979            compat_args: _,
3980        } = args;
3981
3982        Ok(Self {
3983            exclude_editable,
3984            exclude: exclude.into_iter().collect(),
3985            paths,
3986            settings: PipSettings::combine(
3987                PipOptions {
3988                    python: python.and_then(Maybe::into_option),
3989                    system: flag(system, no_system, "system")?,
3990                    strict: flag(strict, no_strict, "strict")?,
3991                    target,
3992                    prefix,
3993                    ..PipOptions::default()
3994                },
3995                filesystem,
3996                environment,
3997            ),
3998        })
3999    }
4000}
4001
4002/// The resolved settings to use for a `pip list` invocation.
4003#[derive(Debug, Clone)]
4004pub struct PipListSettings {
4005    pub editable: Option<bool>,
4006    pub exclude: FxHashSet<PackageName>,
4007    pub format: ListFormat,
4008    pub outdated: bool,
4009    pub settings: PipSettings,
4010}
4011
4012impl PipListSettings {
4013    /// Resolve the [`PipListSettings`] from the CLI and filesystem configuration.
4014    pub fn resolve(
4015        args: PipListArgs,
4016        filesystem: Option<FilesystemOptions>,
4017        environment: EnvironmentOptions,
4018    ) -> anyhow::Result<Self> {
4019        let PipListArgs {
4020            editable,
4021            exclude_editable,
4022            exclude,
4023            format,
4024            outdated,
4025            no_outdated,
4026            strict,
4027            no_strict,
4028            fetch,
4029            python,
4030            system,
4031            no_system,
4032            target,
4033            prefix,
4034            compat_args: _,
4035        } = args;
4036
4037        Ok(Self {
4038            editable: flag(editable, exclude_editable, "exclude-editable")?,
4039            exclude: exclude.into_iter().collect(),
4040            format,
4041            outdated: flag(outdated, no_outdated, "outdated")?.unwrap_or(false),
4042            settings: PipSettings::combine(
4043                PipOptions {
4044                    python: python.and_then(Maybe::into_option),
4045                    system: flag(system, no_system, "system")?,
4046                    strict: flag(strict, no_strict, "strict")?,
4047                    target,
4048                    prefix,
4049                    ..fetch.into_pip_options(configured_indexes(filesystem.as_ref()))?
4050                },
4051                filesystem,
4052                environment,
4053            ),
4054        })
4055    }
4056}
4057
4058/// The resolved settings to use for a `pip show` invocation.
4059#[derive(Debug, Clone)]
4060pub struct PipShowSettings {
4061    pub package: Vec<PackageName>,
4062    pub files: bool,
4063    pub settings: PipSettings,
4064}
4065
4066impl PipShowSettings {
4067    /// Resolve the [`PipShowSettings`] from the CLI and filesystem configuration.
4068    pub fn resolve(
4069        args: PipShowArgs,
4070        filesystem: Option<FilesystemOptions>,
4071        environment: EnvironmentOptions,
4072    ) -> anyhow::Result<Self> {
4073        let PipShowArgs {
4074            package,
4075            strict,
4076            no_strict,
4077            files,
4078            python,
4079            system,
4080            no_system,
4081            target,
4082            prefix,
4083            compat_args: _,
4084        } = args;
4085
4086        Ok(Self {
4087            package,
4088            files,
4089            settings: PipSettings::combine(
4090                PipOptions {
4091                    python: python.and_then(Maybe::into_option),
4092                    system: flag(system, no_system, "system")?,
4093                    strict: flag(strict, no_strict, "strict")?,
4094                    target,
4095                    prefix,
4096                    ..PipOptions::default()
4097                },
4098                filesystem,
4099                environment,
4100            ),
4101        })
4102    }
4103}
4104
4105/// The resolved settings to use for a `pip tree` invocation.
4106#[derive(Debug, Clone)]
4107pub struct PipTreeSettings {
4108    pub show_version_specifiers: bool,
4109    pub depth: u8,
4110    pub prune: Vec<PackageName>,
4111    pub package: Vec<PackageName>,
4112    pub no_dedupe: bool,
4113    pub invert: bool,
4114    pub outdated: bool,
4115    pub settings: PipSettings,
4116}
4117
4118impl PipTreeSettings {
4119    /// Resolve the [`PipTreeSettings`] from the CLI and workspace configuration.
4120    pub fn resolve(
4121        args: PipTreeArgs,
4122        filesystem: Option<FilesystemOptions>,
4123        environment: EnvironmentOptions,
4124    ) -> anyhow::Result<Self> {
4125        let PipTreeArgs {
4126            show_version_specifiers,
4127            tree,
4128            strict,
4129            no_strict,
4130            fetch,
4131            python,
4132            system,
4133            no_system,
4134            compat_args: _,
4135        } = args;
4136
4137        Ok(Self {
4138            show_version_specifiers,
4139            depth: tree.depth,
4140            prune: tree.prune,
4141            no_dedupe: tree.no_dedupe,
4142            invert: tree.invert,
4143            package: tree.package,
4144            outdated: tree.outdated,
4145            settings: PipSettings::combine(
4146                PipOptions {
4147                    python: python.and_then(Maybe::into_option),
4148                    system: flag(system, no_system, "system")?,
4149                    strict: flag(strict, no_strict, "strict")?,
4150                    ..fetch.into_pip_options(configured_indexes(filesystem.as_ref()))?
4151                },
4152                filesystem,
4153                environment,
4154            ),
4155        })
4156    }
4157}
4158
4159/// The resolved settings to use for a `pip check` invocation.
4160#[derive(Debug, Clone)]
4161pub struct PipCheckSettings {
4162    pub settings: PipSettings,
4163}
4164
4165impl PipCheckSettings {
4166    /// Resolve the [`PipCheckSettings`] from the CLI and filesystem configuration.
4167    pub fn resolve(
4168        args: PipCheckArgs,
4169        filesystem: Option<FilesystemOptions>,
4170        environment: EnvironmentOptions,
4171    ) -> anyhow::Result<Self> {
4172        let PipCheckArgs {
4173            python,
4174            system,
4175            no_system,
4176            python_version,
4177            python_platform,
4178        } = args;
4179
4180        Ok(Self {
4181            settings: PipSettings::combine(
4182                PipOptions {
4183                    python: python.and_then(Maybe::into_option),
4184                    system: flag(system, no_system, "system")?,
4185                    python_version,
4186                    python_platform,
4187                    ..PipOptions::default()
4188                },
4189                filesystem,
4190                environment,
4191            ),
4192        })
4193    }
4194}
4195
4196/// The resolved settings to use for a `build` invocation.
4197#[derive(Debug, Clone)]
4198pub struct BuildSettings {
4199    pub skip_dependency_check: bool,
4200    pub src: Option<PathBuf>,
4201    pub package: Option<PackageName>,
4202    pub all_packages: bool,
4203    pub out_dir: Option<PathBuf>,
4204    pub sdist: bool,
4205    pub wheel: bool,
4206    pub list: bool,
4207    pub build_logs: bool,
4208    pub gitignore: bool,
4209    pub force_pep517: bool,
4210    pub clear: bool,
4211    pub build_constraints: Vec<RequirementsInput>,
4212    pub build_constraints_from_workspace: Vec<NameRequirementSpecification>,
4213    pub hash_checking: Option<HashCheckingMode>,
4214    pub python: Option<String>,
4215    pub install_mirrors: PythonInstallMirrors,
4216    pub refresh: Refresh,
4217    pub settings: ResolverSettings,
4218}
4219
4220impl BuildSettings {
4221    /// Resolve the [`BuildSettings`] from the CLI and filesystem configuration.
4222    pub fn resolve(
4223        args: BuildArgs,
4224        filesystem: Option<FilesystemOptions>,
4225        environment: EnvironmentOptions,
4226    ) -> anyhow::Result<Self> {
4227        let BuildArgs {
4228            skip_dependency_check,
4229            src,
4230            out_dir,
4231            package,
4232            all_packages,
4233            sdist,
4234            wheel,
4235            list,
4236            force_pep517,
4237            clear,
4238            build_constraints,
4239            hash_checking:
4240                HashCheckingArgs {
4241                    require_hashes,
4242                    no_require_hashes,
4243                    verify_hashes,
4244                    no_verify_hashes,
4245                },
4246            build_logs,
4247            no_build_logs,
4248            create_gitignore,
4249            no_create_gitignore,
4250            python,
4251            build,
4252            refresh,
4253            resolver,
4254        } = args;
4255        let filesystem_install_mirrors = match &filesystem {
4256            Some(fs) => fs.install_mirrors.clone(),
4257            None => PythonInstallMirrors::default(),
4258        };
4259        let build_constraints_from_workspace = if let Some(configuration) = &filesystem {
4260            configuration
4261                .build_constraint_dependencies
4262                .clone()
4263                .unwrap_or_default()
4264                .into_iter()
4265                .map(|requirement| {
4266                    let (requirement, hashes) = requirement.into_parts();
4267                    NameRequirementSpecification {
4268                        requirement: Requirement::from(
4269                            requirement.with_origin(RequirementOrigin::Workspace),
4270                        ),
4271                        hashes,
4272                    }
4273                })
4274                .collect()
4275        } else {
4276            Vec::new()
4277        };
4278
4279        Ok(Self {
4280            skip_dependency_check,
4281            src,
4282            package,
4283            all_packages,
4284            out_dir,
4285            sdist,
4286            wheel,
4287            list,
4288            build_logs: flag(build_logs, no_build_logs, "build-logs")?.unwrap_or(true),
4289            force_pep517,
4290            clear,
4291            gitignore: flag(create_gitignore, no_create_gitignore, "create-gitignore")?
4292                .unwrap_or(true),
4293            build_constraints: requirement_files(
4294                build_constraints,
4295                environment.build_constraints.clone(),
4296            ),
4297            build_constraints_from_workspace,
4298            hash_checking: HashCheckingMode::from_args(
4299                flag(require_hashes, no_require_hashes, "require-hashes")?,
4300                flag(verify_hashes, no_verify_hashes, "verify-hashes")?,
4301            ),
4302            python: python.and_then(Maybe::into_option),
4303            refresh: Refresh::try_from(refresh)?,
4304            settings: resolve_resolver_settings(resolver, build, filesystem, &environment)?,
4305            install_mirrors: environment
4306                .install_mirrors
4307                .combine(filesystem_install_mirrors),
4308        })
4309    }
4310}
4311
4312/// The resolved settings to use for a `venv` invocation.
4313#[derive(Debug, Clone)]
4314pub struct VenvSettings {
4315    pub seed: bool,
4316    pub allow_existing: bool,
4317    pub clear: bool,
4318    pub force: bool,
4319    pub no_clear: bool,
4320    pub path: Option<PathBuf>,
4321    pub prompt: Option<String>,
4322    pub system_site_packages: bool,
4323    pub relocatable: bool,
4324    pub no_relocatable: bool,
4325    pub no_project: bool,
4326    pub refresh: Refresh,
4327    pub settings: PipSettings,
4328}
4329
4330impl VenvSettings {
4331    /// Resolve the [`VenvSettings`] from the CLI and filesystem configuration.
4332    pub fn resolve(
4333        args: VenvArgs,
4334        filesystem: Option<FilesystemOptions>,
4335        environment: EnvironmentOptions,
4336    ) -> anyhow::Result<Self> {
4337        let VenvArgs {
4338            python,
4339            system,
4340            no_system,
4341            seed,
4342            allow_existing,
4343            clear,
4344            force,
4345            no_clear,
4346            path,
4347            prompt,
4348            system_site_packages,
4349            relocatable,
4350            no_relocatable,
4351            index_args,
4352            registry_client:
4353                RegistryClientArgs {
4354                    index_strategy,
4355                    keyring_provider,
4356                },
4357            exclude_newer:
4358                PackageExcludeNewerArgs {
4359                    exclude_newer: ExcludeNewerArgs { exclude_newer },
4360                    exclude_newer_package,
4361                },
4362            no_project,
4363            link_mode,
4364            refresh,
4365            compat_args: _,
4366        } = args;
4367
4368        // Resolve flags from CLI and environment variables.
4369        let seed = seed || environment.venv_seed.value == Some(true);
4370        let (clear, no_clear) = resolve_flag_pair(
4371            clear,
4372            no_clear,
4373            "clear",
4374            "no-clear",
4375            Some(environment.venv_clear),
4376            None,
4377        );
4378        let (relocatable, no_relocatable) = resolve_flag_pair(
4379            relocatable,
4380            no_relocatable,
4381            "relocatable",
4382            "no-relocatable",
4383            Some(environment.venv_relocatable),
4384            None,
4385        );
4386
4387        Ok(Self {
4388            seed,
4389            allow_existing,
4390            clear: clear.into(),
4391            force,
4392            no_clear: no_clear.into(),
4393            path,
4394            prompt,
4395            system_site_packages,
4396            no_project,
4397            relocatable: relocatable.into(),
4398            no_relocatable: no_relocatable.into(),
4399            refresh: Refresh::try_from(refresh)?,
4400            settings: PipSettings::combine(
4401                PipOptions {
4402                    python: python.and_then(Maybe::into_option),
4403                    system: flag(system, no_system, "system")?,
4404                    index_strategy,
4405                    keyring_provider,
4406                    exclude_newer,
4407                    exclude_newer_package: exclude_newer_package
4408                        .map(ExcludeNewerPackage::from_iter),
4409                    link_mode,
4410                    ..index_args.into_pip_options(configured_indexes(filesystem.as_ref()))?
4411                },
4412                filesystem,
4413                environment,
4414            ),
4415        })
4416    }
4417}
4418
4419/// Resolve the `uv pip` build-hash policy from CLI, environment, and configuration settings.
4420fn resolve_pip_build_hash_checking(
4421    require_build_hashes: Option<bool>,
4422    filesystem: Option<&FilesystemOptions>,
4423    environment: &EnvironmentOptions,
4424) -> HashCheckingMode {
4425    let configured = filesystem.and_then(|filesystem| {
4426        filesystem
4427            .pip
4428            .as_ref()
4429            .and_then(|pip| pip.require_build_hashes)
4430            .or(filesystem.top_level.require_build_hashes)
4431    });
4432    resolve_build_hash_checking(
4433        require_build_hashes
4434            .or(environment.require_build_hashes)
4435            .or(configured),
4436    )
4437}
4438
4439/// Return the indexes from the effective filesystem configuration.
4440fn configured_indexes(filesystem: Option<&FilesystemOptions>) -> &[Index] {
4441    filesystem
4442        .and_then(|options| options.top_level.index.as_deref())
4443        .unwrap_or_default()
4444}
4445
4446/// Resolve the [`ResolverSettings`] from the CLI, environment, and filesystem configuration.
4447fn resolve_resolver_settings(
4448    args: ResolverArgs,
4449    build: BuildOptionsArgs,
4450    filesystem: Option<FilesystemOptions>,
4451    environment: &EnvironmentOptions,
4452) -> Result<ResolverSettings> {
4453    let args = resolver_options(args, build, configured_indexes(filesystem.as_ref()))?;
4454
4455    Ok(combine_resolver_settings(args, filesystem, environment))
4456}
4457
4458/// Resolve the [`ResolverSettings`] from the CLI and filesystem configuration.
4459fn combine_resolver_settings(
4460    mut args: ResolverOptions,
4461    filesystem: Option<FilesystemOptions>,
4462    environment: &EnvironmentOptions,
4463) -> ResolverSettings {
4464    args.require_build_hashes = args
4465        .require_build_hashes
4466        .or(environment.require_build_hashes);
4467    args.no_binary_package = args
4468        .no_binary_package
4469        .or(environment.no_binary_package.clone());
4470    args.no_build_package = args
4471        .no_build_package
4472        .or(environment.no_build_package.clone());
4473    args.no_sources_package = args
4474        .no_sources_package
4475        .or(environment.no_sources_package.clone());
4476
4477    // The problem is that for `upgrade`... we want to combine the two `Upgrade` structs,
4478    // not the individual fields.
4479    let options = args.combine(ResolverOptions::from(
4480        filesystem
4481            .map(FilesystemOptions::into_options)
4482            .map(|options| options.top_level)
4483            .unwrap_or_default(),
4484    ));
4485
4486    ResolverSettings {
4487        cuda_driver_version: environment.cuda_driver_version.clone(),
4488        amd_gpu_architecture: environment.amd_gpu_architecture,
4489        ..ResolverSettings::from(options)
4490    }
4491}
4492
4493/// Resolve the [`ResolverInstallerSettings`] from CLI, environment, and filesystem options.
4494fn resolve_resolver_installer_settings(
4495    args: ResolverInstallerArgs,
4496    build: BuildOptionsArgs,
4497    filesystem: Option<FilesystemOptions>,
4498    environment: &EnvironmentOptions,
4499) -> Result<ResolverInstallerSettings> {
4500    let args = resolver_installer_options(args, build, configured_indexes(filesystem.as_ref()))?;
4501
4502    Ok(combine_resolver_installer_settings(
4503        args,
4504        filesystem,
4505        environment,
4506    ))
4507}
4508
4509/// Reconcile the [`ResolverInstallerSettings`] from the CLI and filesystem configuration.
4510fn combine_resolver_installer_settings(
4511    args: ResolverInstallerOptions,
4512    filesystem: Option<FilesystemOptions>,
4513    environment: &EnvironmentOptions,
4514) -> ResolverInstallerSettings {
4515    let options = resolver_installer_options_with_environment(args, environment).combine(
4516        ResolverInstallerOptions::from(
4517            filesystem
4518                .map(FilesystemOptions::into_options)
4519                .map(|options| options.top_level)
4520                .unwrap_or_default(),
4521        ),
4522    );
4523
4524    let base = ResolverInstallerSettings::from(options);
4525    ResolverInstallerSettings {
4526        resolver: ResolverSettings {
4527            cuda_driver_version: environment.cuda_driver_version.clone(),
4528            amd_gpu_architecture: environment.amd_gpu_architecture,
4529            ..base.resolver
4530        },
4531        ..base
4532    }
4533}
4534
4535fn resolver_installer_options_with_environment(
4536    mut options: ResolverInstallerOptions,
4537    environment: &EnvironmentOptions,
4538) -> ResolverInstallerOptions {
4539    options.require_build_hashes = options
4540        .require_build_hashes
4541        .or(environment.require_build_hashes);
4542    options.no_binary_package = options
4543        .no_binary_package
4544        .or(environment.no_binary_package.clone());
4545    options.no_build_package = options
4546        .no_build_package
4547        .or(environment.no_build_package.clone());
4548    options.no_sources_package = options
4549        .no_sources_package
4550        .or(environment.no_sources_package.clone());
4551    options
4552}
4553
4554/// The resolved settings to use for an invocation of the `pip` CLI.
4555///
4556/// Represents the shared settings that are used across all `pip` commands. Analogous to the
4557/// settings contained in the `[tool.uv.pip]` table.
4558#[derive(Debug, Clone)]
4559pub struct PipSettings {
4560    pub index_locations: IndexLocations,
4561    pub python: Option<String>,
4562    pub install_mirrors: PythonInstallMirrors,
4563    pub system: bool,
4564    pub extras: ExtrasSpecification,
4565    pub groups: Vec<PipGroupName>,
4566    pub break_system_packages: bool,
4567    pub target: Option<Target>,
4568    pub prefix: Option<Prefix>,
4569    pub index_strategy: IndexStrategy,
4570    pub keyring_provider: KeyringProviderType,
4571    pub torch_backend: Option<TorchMode>,
4572    pub cuda_driver_version: Option<Version>,
4573    pub amd_gpu_architecture: Option<AmdGpuArchitecture>,
4574    pub build_isolation: BuildIsolation,
4575    pub extra_build_dependencies: ExtraBuildDependencies,
4576    pub extra_build_variables: ExtraBuildVariables,
4577    pub build_options: BuildOptions,
4578    pub allow_empty_requirements: bool,
4579    pub strict: bool,
4580    pub dependency_mode: DependencyMode,
4581    pub resolution: ResolutionMode,
4582    pub prerelease: Prerelease,
4583    pub fork_strategy: ForkStrategy,
4584    pub dependency_metadata: DependencyMetadata,
4585    pub output_file: Option<PathBuf>,
4586    pub no_strip_extras: bool,
4587    pub no_strip_markers: bool,
4588    pub no_annotate: bool,
4589    pub no_header: bool,
4590    pub custom_compile_command: Option<String>,
4591    pub generate_hashes: bool,
4592    pub config_setting: ConfigSettings,
4593    pub config_settings_package: PackageConfigSettings,
4594    pub python_version: Option<PythonVersion>,
4595    pub python_platform: Option<TargetTriple>,
4596    pub universal: bool,
4597    pub exclude_newer: ExcludeNewer,
4598    pub no_emit_package: Vec<PackageName>,
4599    pub emit_index_url: bool,
4600    pub emit_find_links: bool,
4601    pub emit_build_options: bool,
4602    pub emit_marker_expression: bool,
4603    pub emit_index_annotation: bool,
4604    pub annotation_style: AnnotationStyle,
4605    pub link_mode: LinkMode,
4606    pub compile_bytecode: bool,
4607    pub sources: NoSources,
4608    pub hash_checking: Option<HashCheckingMode>,
4609    pub upgrade: Upgrade,
4610    pub reinstall: Reinstall,
4611}
4612
4613impl PipSettings {
4614    /// Resolve the [`PipSettings`] from the CLI and filesystem configuration.
4615    fn combine(
4616        args: PipOptions,
4617        filesystem: Option<FilesystemOptions>,
4618        environment: EnvironmentOptions,
4619    ) -> Self {
4620        let Options {
4621            top_level,
4622            pip,
4623            install_mirrors: filesystem_install_mirrors,
4624            ..
4625        } = filesystem
4626            .map(FilesystemOptions::into_options)
4627            .unwrap_or_default();
4628
4629        let PipOptions {
4630            python,
4631            system,
4632            break_system_packages,
4633            target,
4634            prefix,
4635            index,
4636            index_url,
4637            extra_index_url,
4638            no_index,
4639            find_links,
4640            index_strategy,
4641            torch_backend,
4642            keyring_provider,
4643            no_build,
4644            no_binary,
4645            only_binary,
4646            no_build_isolation,
4647            require_build_hashes: _,
4648            no_build_isolation_package,
4649            extra_build_dependencies,
4650            extra_build_variables,
4651            strict,
4652            extra,
4653            all_extras,
4654            no_extra,
4655            group,
4656            no_deps,
4657            allow_empty_requirements,
4658            resolution,
4659            prerelease,
4660            prerelease_package: _,
4661            fork_strategy,
4662            dependency_metadata,
4663            output_file,
4664            no_strip_extras,
4665            no_strip_markers,
4666            no_annotate,
4667            no_header,
4668            custom_compile_command,
4669            generate_hashes,
4670            config_settings,
4671            config_settings_package,
4672            python_version,
4673            python_platform,
4674            universal,
4675            exclude_newer,
4676            no_emit_package,
4677            emit_index_url,
4678            emit_find_links,
4679            emit_build_options,
4680            emit_marker_expression,
4681            emit_index_annotation,
4682            annotation_style,
4683            link_mode,
4684            compile_bytecode,
4685            require_hashes,
4686            verify_hashes,
4687            no_sources,
4688            no_sources_package,
4689            upgrade,
4690            upgrade_package,
4691            reinstall,
4692            reinstall_package,
4693            exclude_newer_package,
4694        } = pip.unwrap_or_default();
4695
4696        let ResolverInstallerSchema {
4697            index: top_level_index,
4698            index_url: top_level_index_url,
4699            extra_index_url: top_level_extra_index_url,
4700            no_index: top_level_no_index,
4701            find_links: top_level_find_links,
4702            index_strategy: top_level_index_strategy,
4703            keyring_provider: top_level_keyring_provider,
4704            resolution: top_level_resolution,
4705            prerelease: top_level_prerelease,
4706            prerelease_package: top_level_prerelease_package,
4707            fork_strategy: top_level_fork_strategy,
4708            dependency_metadata: top_level_dependency_metadata,
4709            config_settings: top_level_config_settings,
4710            config_settings_package: top_level_config_settings_package,
4711            no_build_isolation: top_level_no_build_isolation,
4712            no_build_isolation_package: top_level_no_build_isolation_package,
4713            extra_build_dependencies: top_level_extra_build_dependencies,
4714            extra_build_variables: top_level_extra_build_variables,
4715            exclude_newer: top_level_exclude_newer,
4716            link_mode: top_level_link_mode,
4717            compile_bytecode: top_level_compile_bytecode,
4718            no_sources: top_level_no_sources,
4719            no_sources_package: top_level_no_sources_package,
4720            upgrade: top_level_upgrade,
4721            upgrade_package: top_level_upgrade_package,
4722            reinstall: top_level_reinstall,
4723            reinstall_package: top_level_reinstall_package,
4724            no_build: top_level_no_build,
4725            no_build_package: top_level_no_build_package,
4726            no_binary: top_level_no_binary,
4727            no_binary_package: top_level_no_binary_package,
4728            exclude_newer_package: top_level_exclude_newer_package,
4729            torch_backend: top_level_torch_backend,
4730            require_build_hashes: _,
4731        } = top_level;
4732
4733        // Merge the top-level options (`tool.uv`) with the pip-specific options (`tool.uv.pip`),
4734        // preferring the latter.
4735        //
4736        // For example, prefer `tool.uv.pip.index-url` over `tool.uv.index-url`.
4737        let index = index.combine(top_level_index);
4738        let no_index = no_index.combine(top_level_no_index);
4739        let index_url = index_url.combine(top_level_index_url);
4740        let extra_index_url = extra_index_url.combine(top_level_extra_index_url);
4741        let find_links = find_links.combine(top_level_find_links);
4742        let index_strategy = index_strategy.combine(top_level_index_strategy);
4743        let keyring_provider = keyring_provider.combine(top_level_keyring_provider);
4744        let resolution = resolution.combine(top_level_resolution);
4745        let prerelease = prerelease.combine(top_level_prerelease);
4746        let prerelease_package = args
4747            .prerelease_package
4748            .combine(top_level_prerelease_package)
4749            .unwrap_or_default();
4750        let fork_strategy = fork_strategy.combine(top_level_fork_strategy);
4751        let dependency_metadata = dependency_metadata.combine(top_level_dependency_metadata);
4752        let config_settings = config_settings.combine(top_level_config_settings);
4753        let config_settings_package =
4754            config_settings_package.combine(top_level_config_settings_package);
4755        let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation);
4756        let no_build_isolation_package =
4757            no_build_isolation_package.combine(top_level_no_build_isolation_package);
4758        let extra_build_dependencies =
4759            extra_build_dependencies.combine(top_level_extra_build_dependencies);
4760        let extra_build_variables = extra_build_variables.combine(top_level_extra_build_variables);
4761        let exclude_newer = args
4762            .exclude_newer
4763            .combine(exclude_newer)
4764            .combine(top_level_exclude_newer);
4765        let exclude_newer_package = args
4766            .exclude_newer_package
4767            .combine(exclude_newer_package)
4768            .combine(top_level_exclude_newer_package)
4769            .unwrap_or_default();
4770        let link_mode = link_mode.combine(top_level_link_mode);
4771        let compile_bytecode = compile_bytecode.combine(top_level_compile_bytecode);
4772        let no_sources = no_sources.combine(top_level_no_sources);
4773        let no_sources_package = no_sources_package.combine(top_level_no_sources_package);
4774        let upgrade = upgrade.combine(top_level_upgrade);
4775        let upgrade_package = upgrade_package.combine(top_level_upgrade_package);
4776        let reinstall = reinstall.combine(top_level_reinstall);
4777        let reinstall_package = reinstall_package.combine(top_level_reinstall_package);
4778        let torch_backend = torch_backend.combine(top_level_torch_backend);
4779        let args_no_sources_package = args
4780            .no_sources_package
4781            .or(environment.no_sources_package.clone());
4782
4783        Self {
4784            index_locations: IndexLocations::new(
4785                args.index
4786                    .into_iter()
4787                    .flatten()
4788                    .chain(args.extra_index_url.into_iter().flatten().map(Index::from))
4789                    .chain(args.index_url.into_iter().map(Index::from))
4790                    .chain(index.into_iter().flatten())
4791                    .chain(extra_index_url.into_iter().flatten().map(Index::from))
4792                    .chain(index_url.into_iter().map(Index::from))
4793                    .collect(),
4794                args.find_links
4795                    .combine(find_links)
4796                    .into_iter()
4797                    .flatten()
4798                    .map(Index::from)
4799                    .collect(),
4800                args.no_index.combine(no_index).unwrap_or_default(),
4801            ),
4802            extras: ExtrasSpecification::from_args(
4803                args.extra.combine(extra).unwrap_or_default(),
4804                args.no_extra.combine(no_extra).unwrap_or_default(),
4805                // TODO(blueraft): support no_default_extras
4806                false,
4807                // TODO(blueraft): support only_extra
4808                vec![],
4809                args.all_extras.combine(all_extras).unwrap_or_default(),
4810            ),
4811
4812            groups: args.group.combine(group).unwrap_or_default(),
4813            dependency_mode: if args.no_deps.combine(no_deps).unwrap_or_default() {
4814                DependencyMode::Direct
4815            } else {
4816                DependencyMode::Transitive
4817            },
4818            resolution: args.resolution.combine(resolution).unwrap_or_default(),
4819            prerelease: resolve_prerelease(
4820                args.prerelease.combine(prerelease).unwrap_or_default(),
4821                prerelease_package,
4822            ),
4823            fork_strategy: args
4824                .fork_strategy
4825                .combine(fork_strategy)
4826                .unwrap_or_default(),
4827            dependency_metadata: DependencyMetadata::from_entries(
4828                args.dependency_metadata
4829                    .combine(dependency_metadata)
4830                    .unwrap_or_default(),
4831            ),
4832            output_file: args.output_file.combine(output_file),
4833            no_strip_extras: args
4834                .no_strip_extras
4835                .combine(no_strip_extras)
4836                .unwrap_or_default(),
4837            no_strip_markers: args
4838                .no_strip_markers
4839                .combine(no_strip_markers)
4840                .unwrap_or_default(),
4841            no_annotate: args.no_annotate.combine(no_annotate).unwrap_or_default(),
4842            no_header: args.no_header.combine(no_header).unwrap_or_default(),
4843            custom_compile_command: args.custom_compile_command.combine(custom_compile_command),
4844            annotation_style: args
4845                .annotation_style
4846                .combine(annotation_style)
4847                .unwrap_or_default(),
4848            index_strategy: args
4849                .index_strategy
4850                .combine(index_strategy)
4851                .unwrap_or_default(),
4852            keyring_provider: args
4853                .keyring_provider
4854                .combine(keyring_provider)
4855                .unwrap_or_default(),
4856            generate_hashes: args
4857                .generate_hashes
4858                .combine(generate_hashes)
4859                .unwrap_or_default(),
4860            allow_empty_requirements: args
4861                .allow_empty_requirements
4862                .combine(allow_empty_requirements)
4863                .unwrap_or_default(),
4864            build_isolation: BuildIsolation::from_args(
4865                args.no_build_isolation,
4866                args.no_build_isolation_package.unwrap_or_default(),
4867            )
4868            .combine(BuildIsolation::from_args(
4869                no_build_isolation,
4870                no_build_isolation_package.unwrap_or_default(),
4871            ))
4872            .unwrap_or_default(),
4873            extra_build_dependencies: args
4874                .extra_build_dependencies
4875                .combine(extra_build_dependencies)
4876                .unwrap_or_default(),
4877            extra_build_variables: args
4878                .extra_build_variables
4879                .combine(extra_build_variables)
4880                .unwrap_or_default(),
4881            config_setting: args
4882                .config_settings
4883                .combine(config_settings)
4884                .unwrap_or_default(),
4885            config_settings_package: args
4886                .config_settings_package
4887                .combine(config_settings_package)
4888                .unwrap_or_default(),
4889            torch_backend: args.torch_backend.combine(torch_backend),
4890            cuda_driver_version: environment.cuda_driver_version.clone(),
4891            amd_gpu_architecture: environment.amd_gpu_architecture,
4892            python_version: args.python_version.combine(python_version),
4893            python_platform: args.python_platform.combine(python_platform),
4894            universal: args.universal.combine(universal).unwrap_or_default(),
4895            exclude_newer: ExcludeNewer::from_args(
4896                exclude_newer,
4897                exclude_newer_package.into_iter().map(Into::into).collect(),
4898            ),
4899            no_emit_package: args
4900                .no_emit_package
4901                .combine(no_emit_package)
4902                .unwrap_or_default(),
4903            emit_index_url: args
4904                .emit_index_url
4905                .combine(emit_index_url)
4906                .unwrap_or_default(),
4907            emit_find_links: args
4908                .emit_find_links
4909                .combine(emit_find_links)
4910                .unwrap_or_default(),
4911            emit_build_options: args
4912                .emit_build_options
4913                .combine(emit_build_options)
4914                .unwrap_or_default(),
4915            emit_marker_expression: args
4916                .emit_marker_expression
4917                .combine(emit_marker_expression)
4918                .unwrap_or_default(),
4919            emit_index_annotation: args
4920                .emit_index_annotation
4921                .combine(emit_index_annotation)
4922                .unwrap_or_default(),
4923            link_mode: args.link_mode.combine(link_mode).unwrap_or_default(),
4924            hash_checking: HashCheckingMode::from_args(
4925                args.require_hashes.combine(require_hashes),
4926                args.verify_hashes.combine(verify_hashes),
4927            ),
4928            python: args.python.combine(python),
4929            system: args.system.combine(system).unwrap_or_default(),
4930            break_system_packages: args
4931                .break_system_packages
4932                .combine(break_system_packages)
4933                .unwrap_or_default(),
4934            target: args.target.combine(target).map(Target::from),
4935            prefix: args.prefix.combine(prefix).map(Prefix::from),
4936            compile_bytecode: args
4937                .compile_bytecode
4938                .combine(compile_bytecode)
4939                .unwrap_or_default(),
4940            sources: NoSources::from_args(
4941                args.no_sources.combine(no_sources),
4942                args_no_sources_package
4943                    .combine(no_sources_package)
4944                    .unwrap_or_default(),
4945            ),
4946            strict: args.strict.combine(strict).unwrap_or_default(),
4947            upgrade: Upgrade::from_args(
4948                args.upgrade,
4949                args.upgrade_package
4950                    .into_iter()
4951                    .flatten()
4952                    .map(Requirement::from)
4953                    .collect(),
4954                Vec::new(),
4955            )
4956            .combine(Upgrade::from_args(
4957                upgrade,
4958                upgrade_package
4959                    .into_iter()
4960                    .flatten()
4961                    .map(Requirement::from)
4962                    .collect(),
4963                Vec::new(),
4964            ))
4965            .unwrap_or_default(),
4966            reinstall: Reinstall::from_args(
4967                args.reinstall,
4968                args.reinstall_package.unwrap_or_default(),
4969            )
4970            .combine(Reinstall::from_args(
4971                reinstall,
4972                reinstall_package.unwrap_or_default(),
4973            ))
4974            .unwrap_or_default(),
4975            build_options: BuildOptions::new(
4976                NoBinary::from_pip_args(args.no_binary.combine(no_binary).unwrap_or_default())
4977                    .combine(NoBinary::from_args(
4978                        top_level_no_binary,
4979                        top_level_no_binary_package.unwrap_or_default(),
4980                    )),
4981                NoBuild::from_pip_args(
4982                    args.only_binary.combine(only_binary).unwrap_or_default(),
4983                    args.no_build.combine(no_build).unwrap_or_default(),
4984                )
4985                .combine(NoBuild::from_args(
4986                    top_level_no_build,
4987                    top_level_no_build_package.unwrap_or_default(),
4988                )),
4989            ),
4990            install_mirrors: environment
4991                .install_mirrors
4992                .combine(filesystem_install_mirrors),
4993        }
4994    }
4995}
4996
4997/// The resolved settings to use for an invocation of the `uv publish` CLI.
4998#[derive(Clone)]
4999pub struct PublishSettings {
5000    // CLI only, see [`PublishArgs`] for docs.
5001    pub files: Vec<String>,
5002    pub username: Option<String>,
5003    pub password: Option<String>,
5004    pub index: Option<String>,
5005    pub dry_run: bool,
5006    pub no_attestations: bool,
5007
5008    // Both CLI and configuration.
5009    pub publish_url: SafeUrl,
5010    pub trusted_publishing: TrustedPublishing,
5011    pub keyring_provider: KeyringProviderType,
5012    pub check_url: Option<IndexUrl>,
5013
5014    // Configuration only
5015    pub index_locations: IndexLocations,
5016}
5017
5018impl fmt::Debug for PublishSettings {
5019    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
5020        f.debug_struct("PublishSettings")
5021            .field("files", &self.files)
5022            .field("username", &self.username)
5023            .field("password", &self.password.as_ref().map(|_| "****"))
5024            .field("index", &self.index)
5025            .field("dry_run", &self.dry_run)
5026            .field("no_attestations", &self.no_attestations)
5027            .field("publish_url", &self.publish_url)
5028            .field("trusted_publishing", &self.trusted_publishing)
5029            .field("keyring_provider", &self.keyring_provider)
5030            .field("check_url", &self.check_url)
5031            .field("index_locations", &self.index_locations)
5032            .finish()
5033    }
5034}
5035
5036impl PublishSettings {
5037    /// Resolve the [`PublishSettings`] from the CLI and filesystem configuration.
5038    pub fn resolve(args: PublishArgs, filesystem: Option<FilesystemOptions>) -> Self {
5039        let Options {
5040            publish, top_level, ..
5041        } = filesystem
5042            .map(FilesystemOptions::into_options)
5043            .unwrap_or_default();
5044
5045        let PublishOptions {
5046            publish_url,
5047            trusted_publishing,
5048            check_url,
5049        } = publish;
5050        let ResolverInstallerSchema {
5051            keyring_provider,
5052            index,
5053            extra_index_url,
5054            index_url,
5055            ..
5056        } = top_level;
5057
5058        // Tokens are encoded in the same way as username/password
5059        let (username, password) = if let Some(token) = args.token {
5060            (Some("__token__".to_string()), Some(token))
5061        } else {
5062            (args.username, args.password)
5063        };
5064
5065        Self {
5066            files: args.files,
5067            username,
5068            password,
5069            dry_run: args.dry_run,
5070            no_attestations: args.no_attestations,
5071            publish_url: args
5072                .publish_url
5073                .combine(publish_url)
5074                .unwrap_or_else(|| SafeUrl::parse(PYPI_PUBLISH_URL).unwrap()),
5075            trusted_publishing: args
5076                .trusted_publishing
5077                .combine(trusted_publishing)
5078                .unwrap_or_default(),
5079            keyring_provider: args
5080                .keyring_provider
5081                .combine(keyring_provider)
5082                .unwrap_or_default(),
5083            check_url: args.check_url.combine(check_url),
5084            index: args.index,
5085            index_locations: IndexLocations::new(
5086                index
5087                    .into_iter()
5088                    .flatten()
5089                    .chain(extra_index_url.into_iter().flatten().map(Index::from))
5090                    .chain(index_url.into_iter().map(Index::from))
5091                    .collect(),
5092                Vec::new(),
5093                false,
5094            ),
5095        }
5096    }
5097}
5098
5099/// The resolved settings to use for an invocation of the `uv auth logout` CLI.
5100#[derive(Debug, Clone)]
5101pub struct AuthLogoutSettings {
5102    pub service: Service,
5103    pub username: Option<String>,
5104}
5105
5106impl AuthLogoutSettings {
5107    /// Resolve the [`AuthLogoutSettings`] from the CLI and filesystem configuration.
5108    pub fn resolve(args: AuthLogoutArgs) -> Self {
5109        Self {
5110            service: args.service,
5111            username: args.username,
5112        }
5113    }
5114}
5115
5116/// The resolved settings to use for an invocation of the `uv auth token` CLI.
5117#[derive(Debug, Clone)]
5118pub struct AuthTokenSettings {
5119    pub service: Service,
5120    pub username: Option<String>,
5121}
5122
5123impl AuthTokenSettings {
5124    /// Resolve the [`AuthTokenSettings`] from the CLI and filesystem configuration.
5125    pub fn resolve(args: AuthTokenArgs) -> Self {
5126        Self {
5127            service: args.service,
5128            username: args.username,
5129        }
5130    }
5131}
5132
5133/// The resolved settings to use for an invocation of the `uv auth set` CLI.
5134#[derive(Debug, Clone)]
5135pub struct AuthLoginSettings {
5136    pub service: Service,
5137    pub username: Option<String>,
5138    pub password: Option<String>,
5139    pub token: Option<String>,
5140}
5141
5142impl AuthLoginSettings {
5143    /// Resolve the [`AuthLoginSettings`] from the CLI and filesystem configuration.
5144    pub fn resolve(args: AuthLoginArgs) -> Self {
5145        Self {
5146            service: args.service,
5147            username: args.username,
5148            password: args.password,
5149            token: args.token,
5150        }
5151    }
5152}
5153
5154// Environment variables that are not exposed as CLI arguments.
5155mod env {
5156    use uv_static::EnvVars;
5157    pub(super) const UV_PYTHON_DOWNLOADS: (&str, &str) = (
5158        EnvVars::UV_PYTHON_DOWNLOADS,
5159        "one of 'auto', 'true', 'manual', 'never', or 'false'",
5160    );
5161}
5162
5163/// Attempt to load and parse an environment variable with the given name.
5164///
5165/// Exits the program and prints an error message containing the expected type if
5166/// parsing values.
5167fn env<T>((name, expected): (&str, &str)) -> Option<T>
5168where
5169    T: FromStr,
5170{
5171    let val = match std::env::var(name) {
5172        Ok(val) => val,
5173        Err(VarError::NotPresent) => return None,
5174        Err(VarError::NotUnicode(_)) => parse_failure(name, expected),
5175    };
5176    Some(
5177        val.parse()
5178            .unwrap_or_else(|_| parse_failure(name, expected)),
5179    )
5180}
5181
5182/// Prints a parse error and exits the process.
5183#[expect(clippy::exit, clippy::print_stderr)]
5184fn parse_failure(name: &str, expected: &str) -> ! {
5185    eprintln!("error: invalid value for {name}, expected {expected}");
5186    process::exit(1)
5187}
5188
5189#[cfg(test)]
5190mod tests {
5191    use crate::{IndexArgs, RegistryClientArgs};
5192
5193    use super::*;
5194
5195    #[test]
5196    fn upgrade_settings_target_only_requested_package() -> anyhow::Result<()> {
5197        let package = PackageName::from_str("anyio")?;
5198        let settings = UpgradeSettings::resolve(
5199            UpgradeArgs {
5200                packages: vec![package.clone()],
5201                exclude: Vec::new(),
5202                index_args: IndexArgs {
5203                    index: None,
5204                    default_index: None,
5205                    index_url: None,
5206                    extra_index_url: None,
5207                    find_links: None,
5208                    no_index: false,
5209                },
5210                registry_client: RegistryClientArgs {
5211                    index_strategy: None,
5212                    keyring_provider: None,
5213                },
5214                require_build_hashes: false,
5215                no_require_build_hashes: false,
5216            },
5217            None,
5218            EnvironmentOptions::new()?,
5219        )?;
5220        let expected = FxHashSet::from_iter([package]);
5221
5222        assert!(!settings.settings.upgrade.is_all());
5223        assert_eq!(settings.settings.upgrade.packages(), Some(&expected));
5224        Ok(())
5225    }
5226}