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