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