Skip to main content

uv_settings/
lib.rs

1use std::num::NonZeroUsize;
2use std::ops::Deref;
3use std::path::{Path, PathBuf};
4use std::time::Duration;
5use uv_client::{DEFAULT_CONNECT_TIMEOUT, DEFAULT_READ_TIMEOUT, DEFAULT_READ_TIMEOUT_UPLOAD};
6use uv_dirs::{system_config_file, user_config_dir};
7use uv_distribution_types::Origin;
8use uv_flags::EnvironmentFlags;
9use uv_fs::Simplified;
10use uv_static::{EnvVars, InvalidEnvironmentVariable, parse_boolish_environment_variable};
11use uv_warnings::warn_user;
12
13pub use crate::combine::*;
14pub use crate::settings::*;
15
16mod combine;
17mod settings;
18
19/// The [`Options`] as loaded from a configuration file on disk.
20#[derive(Debug, Clone)]
21pub struct FilesystemOptions(Options);
22
23impl FilesystemOptions {
24    /// Convert the [`FilesystemOptions`] into [`Options`].
25    pub fn into_options(self) -> Options {
26        self.0
27    }
28
29    /// Set the [`Origin`] on all indexes without an existing origin.
30    #[must_use]
31    pub fn with_origin(self, origin: Origin) -> Self {
32        Self(self.0.with_origin(origin))
33    }
34}
35
36impl Deref for FilesystemOptions {
37    type Target = Options;
38
39    fn deref(&self) -> &Self::Target {
40        &self.0
41    }
42}
43
44impl FilesystemOptions {
45    /// Load the user [`FilesystemOptions`].
46    pub fn user() -> Result<Option<Self>, Error> {
47        let Some(dir) = user_config_dir() else {
48            return Ok(None);
49        };
50        let root = dir.join("uv");
51        let file = root.join("uv.toml");
52
53        tracing::debug!("Searching for user configuration in: `{}`", file.display());
54        match read_file(&file) {
55            Ok(options) => {
56                tracing::debug!("Found user configuration in: `{}`", file.display());
57                validate_uv_toml(&file, &options)?;
58                Ok(Some(Self(options.with_origin(Origin::User))))
59            }
60            Err(Error::Io(err))
61                if matches!(
62                    err.kind(),
63                    std::io::ErrorKind::NotFound
64                        | std::io::ErrorKind::NotADirectory
65                        | std::io::ErrorKind::PermissionDenied
66                ) =>
67            {
68                Ok(None)
69            }
70            Err(err) => Err(err),
71        }
72    }
73
74    pub fn system() -> Result<Option<Self>, Error> {
75        let Some(file) = system_config_file() else {
76            return Ok(None);
77        };
78
79        tracing::debug!("Found system configuration in: `{}`", file.display());
80        let options = read_file(&file)?;
81        validate_uv_toml(&file, &options)?;
82        Ok(Some(Self(options.with_origin(Origin::System))))
83    }
84
85    /// Find the [`FilesystemOptions`] for the given path.
86    ///
87    /// The search starts at the given path and goes up the directory tree until a `uv.toml` file or
88    /// `pyproject.toml` file is found.
89    pub fn find(path: &Path) -> Result<Option<Self>, Error> {
90        for ancestor in path.ancestors() {
91            match Self::from_directory(ancestor) {
92                Ok(Some(options)) => {
93                    return Ok(Some(options));
94                }
95                Ok(None) => {
96                    // Continue traversing the directory tree.
97                }
98                Err(Error::PyprojectToml(path, err)) => {
99                    // If we see an invalid `pyproject.toml`, warn but continue.
100                    warn_user!(
101                        "Failed to parse `{}` during settings discovery:\n{}",
102                        path.user_display().cyan(),
103                        textwrap::indent(&err.to_string(), "  ")
104                    );
105                }
106                Err(err) => {
107                    // Otherwise, warn and stop.
108                    return Err(err);
109                }
110            }
111        }
112        Ok(None)
113    }
114
115    /// Load a [`FilesystemOptions`] from a directory, preferring a `uv.toml` file over a
116    /// `pyproject.toml` file.
117    pub fn from_directory(dir: &Path) -> Result<Option<Self>, Error> {
118        // Read a `uv.toml` file in the current directory.
119        let path = dir.join("uv.toml");
120        match fs_err::read_to_string(&path) {
121            Ok(content) => {
122                let options = toml::from_str::<Options>(&content)
123                    .map_err(|err| Error::UvToml(path.clone(), Box::new(err)))?
124                    .relative_to(&std::path::absolute(dir)?)?;
125
126                // If the directory also contains a `[tool.uv]` table in a `pyproject.toml` file,
127                // warn.
128                let pyproject = dir.join("pyproject.toml");
129                if let Some(pyproject) = fs_err::read_to_string(pyproject)
130                    .ok()
131                    .and_then(|content| toml::from_str::<PyProjectToml>(&content).ok())
132                {
133                    if let Some(options) = pyproject.tool.as_ref().and_then(|tool| tool.uv.as_ref())
134                    {
135                        warn_uv_toml_masked_fields(options);
136                    }
137                }
138
139                tracing::debug!("Found workspace configuration at `{}`", path.display());
140                validate_uv_toml(&path, &options)?;
141                return Ok(Some(Self(options.with_origin(Origin::Project))));
142            }
143            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
144            Err(err) => return Err(err.into()),
145        }
146
147        // Read a `pyproject.toml` file in the current directory.
148        let path = dir.join("pyproject.toml");
149        match fs_err::read_to_string(&path) {
150            Ok(content) => {
151                // Parse, but skip any `pyproject.toml` that doesn't have a `[tool.uv]` section.
152                let pyproject: PyProjectToml = toml::from_str(&content)
153                    .map_err(|err| Error::PyprojectToml(path.clone(), Box::new(err)))?;
154                let Some(tool) = pyproject.tool else {
155                    tracing::debug!(
156                        "Skipping `pyproject.toml` in `{}` (no `[tool]` section)",
157                        dir.display()
158                    );
159                    return Ok(None);
160                };
161                let Some(options) = tool.uv else {
162                    tracing::debug!(
163                        "Skipping `pyproject.toml` in `{}` (no `[tool.uv]` section)",
164                        dir.display()
165                    );
166                    return Ok(None);
167                };
168
169                let options = options.relative_to(&std::path::absolute(dir)?)?;
170
171                tracing::debug!("Found workspace configuration at `{}`", path.display());
172                return Ok(Some(Self(options)));
173            }
174            Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}
175            Err(err) => return Err(err.into()),
176        }
177
178        Ok(None)
179    }
180
181    /// Load a [`FilesystemOptions`] from a `uv.toml` file.
182    pub fn from_file(path: impl AsRef<Path>) -> Result<Self, Error> {
183        let path = path.as_ref();
184        tracing::debug!("Reading user configuration from: `{}`", path.display());
185
186        let options = read_file(path)?;
187        validate_uv_toml(path, &options)?;
188        Ok(Self(options))
189    }
190}
191
192impl From<Options> for FilesystemOptions {
193    fn from(options: Options) -> Self {
194        Self(options)
195    }
196}
197
198/// Load [`Options`] from a `uv.toml` file.
199fn read_file(path: &Path) -> Result<Options, Error> {
200    let content = fs_err::read_to_string(path)?;
201    let options = toml::from_str::<Options>(&content)
202        .map_err(|err| Error::UvToml(path.to_path_buf(), Box::new(err)))?;
203    let options = if let Some(parent) = std::path::absolute(path)?.parent() {
204        options.relative_to(parent)?
205    } else {
206        options
207    };
208    Ok(options)
209}
210
211/// Validate that an [`Options`] schema is compatible with `uv.toml`.
212fn validate_uv_toml(path: &Path, options: &Options) -> Result<(), Error> {
213    let Options {
214        globals: _,
215        top_level: _,
216        install_mirrors: _,
217        publish: _,
218        add: _,
219        pip: _,
220        cache_keys: _,
221        override_dependencies: _,
222        exclude_dependencies: _,
223        constraint_dependencies: _,
224        build_constraint_dependencies: _,
225        environments,
226        required_environments,
227        conflicts,
228        workspace,
229        sources,
230        dev_dependencies,
231        default_groups,
232        dependency_groups,
233        managed,
234        package,
235        build_backend,
236    } = options;
237    // The `uv.toml` format is not allowed to include any of the following, which are
238    // permitted by the schema since they _can_ be included in `pyproject.toml` files
239    // (and we want to use `deny_unknown_fields`).
240    if conflicts.is_some() {
241        return Err(Error::PyprojectOnlyField(path.to_path_buf(), "conflicts"));
242    }
243    if workspace.is_some() {
244        return Err(Error::PyprojectOnlyField(path.to_path_buf(), "workspace"));
245    }
246    if sources.is_some() {
247        return Err(Error::PyprojectOnlyField(path.to_path_buf(), "sources"));
248    }
249    if dev_dependencies.is_some() {
250        return Err(Error::PyprojectOnlyField(
251            path.to_path_buf(),
252            "dev-dependencies",
253        ));
254    }
255    if default_groups.is_some() {
256        return Err(Error::PyprojectOnlyField(
257            path.to_path_buf(),
258            "default-groups",
259        ));
260    }
261    if dependency_groups.is_some() {
262        return Err(Error::PyprojectOnlyField(
263            path.to_path_buf(),
264            "dependency-groups",
265        ));
266    }
267    if managed.is_some() {
268        return Err(Error::PyprojectOnlyField(path.to_path_buf(), "managed"));
269    }
270    if package.is_some() {
271        return Err(Error::PyprojectOnlyField(path.to_path_buf(), "package"));
272    }
273    if build_backend.is_some() {
274        return Err(Error::PyprojectOnlyField(
275            path.to_path_buf(),
276            "build-backend",
277        ));
278    }
279    if environments.is_some() {
280        return Err(Error::PyprojectOnlyField(
281            path.to_path_buf(),
282            "environments",
283        ));
284    }
285    if required_environments.is_some() {
286        return Err(Error::PyprojectOnlyField(
287            path.to_path_buf(),
288            "required-environments",
289        ));
290    }
291    Ok(())
292}
293
294/// Validate that an [`Options`] contains no fields that `uv.toml` would mask
295///
296/// This is essentially the inverse of [`validate_uv_toml`].
297fn warn_uv_toml_masked_fields(options: &Options) {
298    let Options {
299        globals:
300            GlobalOptions {
301                required_version,
302                native_tls,
303                offline,
304                no_cache,
305                cache_dir,
306                preview,
307                python_preference,
308                python_downloads,
309                concurrent_downloads,
310                concurrent_builds,
311                concurrent_installs,
312                allow_insecure_host,
313                http_proxy,
314                https_proxy,
315                no_proxy,
316            },
317        top_level:
318            ResolverInstallerSchema {
319                index,
320                index_url,
321                extra_index_url,
322                no_index,
323                find_links,
324                index_strategy,
325                keyring_provider,
326                resolution,
327                prerelease,
328                fork_strategy,
329                dependency_metadata,
330                config_settings,
331                config_settings_package,
332                no_build_isolation,
333                no_build_isolation_package,
334                extra_build_dependencies,
335                extra_build_variables,
336                exclude_newer,
337                exclude_newer_package,
338                link_mode,
339                compile_bytecode,
340                no_sources,
341                no_sources_package: _,
342                upgrade,
343                upgrade_package,
344                reinstall,
345                reinstall_package,
346                no_build,
347                no_build_package,
348                no_binary,
349                no_binary_package,
350                torch_backend,
351            },
352        install_mirrors:
353            PythonInstallMirrors {
354                python_install_mirror,
355                pypy_install_mirror,
356                python_downloads_json_url,
357            },
358        publish:
359            PublishOptions {
360                publish_url,
361                trusted_publishing,
362                check_url,
363            },
364        add: AddOptions { add_bounds },
365        pip,
366        cache_keys,
367        override_dependencies,
368        exclude_dependencies,
369        constraint_dependencies,
370        build_constraint_dependencies,
371        environments: _,
372        required_environments: _,
373        conflicts: _,
374        workspace: _,
375        sources: _,
376        dev_dependencies: _,
377        default_groups: _,
378        dependency_groups: _,
379        managed: _,
380        package: _,
381        build_backend: _,
382    } = options;
383
384    let mut masked_fields = vec![];
385
386    if required_version.is_some() {
387        masked_fields.push("required-version");
388    }
389    if native_tls.is_some() {
390        masked_fields.push("native-tls");
391    }
392    if offline.is_some() {
393        masked_fields.push("offline");
394    }
395    if no_cache.is_some() {
396        masked_fields.push("no-cache");
397    }
398    if cache_dir.is_some() {
399        masked_fields.push("cache-dir");
400    }
401    if preview.is_some() {
402        masked_fields.push("preview");
403    }
404    if python_preference.is_some() {
405        masked_fields.push("python-preference");
406    }
407    if python_downloads.is_some() {
408        masked_fields.push("python-downloads");
409    }
410    if concurrent_downloads.is_some() {
411        masked_fields.push("concurrent-downloads");
412    }
413    if concurrent_builds.is_some() {
414        masked_fields.push("concurrent-builds");
415    }
416    if concurrent_installs.is_some() {
417        masked_fields.push("concurrent-installs");
418    }
419    if allow_insecure_host.is_some() {
420        masked_fields.push("allow-insecure-host");
421    }
422    if http_proxy.is_some() {
423        masked_fields.push("http-proxy");
424    }
425    if https_proxy.is_some() {
426        masked_fields.push("https-proxy");
427    }
428    if no_proxy.is_some() {
429        masked_fields.push("no-proxy");
430    }
431    if index.is_some() {
432        masked_fields.push("index");
433    }
434    if index_url.is_some() {
435        masked_fields.push("index-url");
436    }
437    if extra_index_url.is_some() {
438        masked_fields.push("extra-index-url");
439    }
440    if no_index.is_some() {
441        masked_fields.push("no-index");
442    }
443    if find_links.is_some() {
444        masked_fields.push("find-links");
445    }
446    if index_strategy.is_some() {
447        masked_fields.push("index-strategy");
448    }
449    if keyring_provider.is_some() {
450        masked_fields.push("keyring-provider");
451    }
452    if resolution.is_some() {
453        masked_fields.push("resolution");
454    }
455    if prerelease.is_some() {
456        masked_fields.push("prerelease");
457    }
458    if fork_strategy.is_some() {
459        masked_fields.push("fork-strategy");
460    }
461    if dependency_metadata.is_some() {
462        masked_fields.push("dependency-metadata");
463    }
464    if config_settings.is_some() {
465        masked_fields.push("config-settings");
466    }
467    if config_settings_package.is_some() {
468        masked_fields.push("config-settings-package");
469    }
470    if no_build_isolation.is_some() {
471        masked_fields.push("no-build-isolation");
472    }
473    if no_build_isolation_package.is_some() {
474        masked_fields.push("no-build-isolation-package");
475    }
476    if extra_build_dependencies.is_some() {
477        masked_fields.push("extra-build-dependencies");
478    }
479    if extra_build_variables.is_some() {
480        masked_fields.push("extra-build-variables");
481    }
482    if exclude_newer.is_some() {
483        masked_fields.push("exclude-newer");
484    }
485    if exclude_newer_package.is_some() {
486        masked_fields.push("exclude-newer-package");
487    }
488    if link_mode.is_some() {
489        masked_fields.push("link-mode");
490    }
491    if compile_bytecode.is_some() {
492        masked_fields.push("compile-bytecode");
493    }
494    if no_sources.is_some() {
495        masked_fields.push("no-sources");
496    }
497    if upgrade.is_some() {
498        masked_fields.push("upgrade");
499    }
500    if upgrade_package.is_some() {
501        masked_fields.push("upgrade-package");
502    }
503    if reinstall.is_some() {
504        masked_fields.push("reinstall");
505    }
506    if reinstall_package.is_some() {
507        masked_fields.push("reinstall-package");
508    }
509    if no_build.is_some() {
510        masked_fields.push("no-build");
511    }
512    if no_build_package.is_some() {
513        masked_fields.push("no-build-package");
514    }
515    if no_binary.is_some() {
516        masked_fields.push("no-binary");
517    }
518    if no_binary_package.is_some() {
519        masked_fields.push("no-binary-package");
520    }
521    if torch_backend.is_some() {
522        masked_fields.push("torch-backend");
523    }
524    if python_install_mirror.is_some() {
525        masked_fields.push("python-install-mirror");
526    }
527    if pypy_install_mirror.is_some() {
528        masked_fields.push("pypy-install-mirror");
529    }
530    if python_downloads_json_url.is_some() {
531        masked_fields.push("python-downloads-json-url");
532    }
533    if publish_url.is_some() {
534        masked_fields.push("publish-url");
535    }
536    if trusted_publishing.is_some() {
537        masked_fields.push("trusted-publishing");
538    }
539    if check_url.is_some() {
540        masked_fields.push("check-url");
541    }
542    if add_bounds.is_some() {
543        masked_fields.push("add-bounds");
544    }
545    if pip.is_some() {
546        masked_fields.push("pip");
547    }
548    if cache_keys.is_some() {
549        masked_fields.push("cache_keys");
550    }
551    if override_dependencies.is_some() {
552        masked_fields.push("override-dependencies");
553    }
554    if exclude_dependencies.is_some() {
555        masked_fields.push("exclude-dependencies");
556    }
557    if constraint_dependencies.is_some() {
558        masked_fields.push("constraint-dependencies");
559    }
560    if build_constraint_dependencies.is_some() {
561        masked_fields.push("build-constraint-dependencies");
562    }
563    if !masked_fields.is_empty() {
564        let field_listing = masked_fields.join("\n- ");
565        warn_user!(
566            "Found both a `uv.toml` file and a `[tool.uv]` section in an adjacent `pyproject.toml`. The following fields from `[tool.uv]` will be ignored in favor of the `uv.toml` file:\n- {}",
567            field_listing,
568        );
569    }
570}
571
572#[derive(thiserror::Error, Debug)]
573pub enum Error {
574    #[error(transparent)]
575    Io(#[from] std::io::Error),
576
577    #[error(transparent)]
578    Index(#[from] uv_distribution_types::IndexUrlError),
579
580    #[error("Failed to parse: `{}`", _0.user_display())]
581    PyprojectToml(PathBuf, #[source] Box<toml::de::Error>),
582
583    #[error("Failed to parse: `{}`", _0.user_display())]
584    UvToml(PathBuf, #[source] Box<toml::de::Error>),
585
586    #[error("Failed to parse: `{}`. The `{}` field is not allowed in a `uv.toml` file. `{}` is only applicable in the context of a project, and should be placed in a `pyproject.toml` file instead.", _0.user_display(), _1, _1
587    )]
588    PyprojectOnlyField(PathBuf, &'static str),
589
590    #[error(transparent)]
591    InvalidEnvironmentVariable(#[from] InvalidEnvironmentVariable),
592}
593
594#[derive(Copy, Clone, Debug)]
595pub struct Concurrency {
596    pub downloads: Option<NonZeroUsize>,
597    pub builds: Option<NonZeroUsize>,
598    pub installs: Option<NonZeroUsize>,
599}
600
601/// A boolean flag parsed from an environment variable.
602///
603/// Stores both the value and the environment variable name for use in error messages.
604#[derive(Debug, Clone, Copy)]
605pub struct EnvFlag {
606    pub value: Option<bool>,
607    pub env_var: &'static str,
608}
609
610impl EnvFlag {
611    /// Create a new [`EnvFlag`] by parsing the given environment variable.
612    pub fn new(env_var: &'static str) -> Result<Self, Error> {
613        Ok(Self {
614            value: parse_boolish_environment_variable(env_var)?,
615            env_var,
616        })
617    }
618}
619
620/// Options loaded from environment variables.
621///
622/// This is currently a subset of all respected environment variables, most are parsed via Clap at
623/// the CLI level, however there are limited semantics in that context.
624#[derive(Debug, Clone)]
625pub struct EnvironmentOptions {
626    pub skip_wheel_filename_check: Option<bool>,
627    pub hide_build_output: Option<bool>,
628    pub python_install_bin: Option<bool>,
629    pub python_install_registry: Option<bool>,
630    pub install_mirrors: PythonInstallMirrors,
631    pub log_context: Option<bool>,
632    pub lfs: Option<bool>,
633    pub http_connect_timeout: Duration,
634    pub http_read_timeout: Duration,
635    /// There's no upload timeout in reqwest, instead we have to use a read timeout as upload
636    /// timeout.
637    pub http_read_timeout_upload: Duration,
638    pub http_retries: u32,
639    pub concurrency: Concurrency,
640    #[cfg(feature = "tracing-durations-export")]
641    pub tracing_durations_file: Option<PathBuf>,
642    pub frozen: EnvFlag,
643    pub locked: EnvFlag,
644    pub offline: EnvFlag,
645    pub no_sync: EnvFlag,
646    pub managed_python: EnvFlag,
647    pub no_managed_python: EnvFlag,
648    pub native_tls: EnvFlag,
649    pub preview: EnvFlag,
650    pub isolated: EnvFlag,
651    pub no_progress: EnvFlag,
652    pub no_installer_metadata: EnvFlag,
653    pub dev: EnvFlag,
654    pub no_dev: EnvFlag,
655    pub show_resolution: EnvFlag,
656    pub no_editable: EnvFlag,
657    pub no_env_file: EnvFlag,
658    pub venv_seed: EnvFlag,
659    pub venv_clear: EnvFlag,
660}
661
662impl EnvironmentOptions {
663    /// Create a new [`EnvironmentOptions`] from environment variables.
664    pub fn new() -> Result<Self, Error> {
665        // Timeout options, matching https://doc.rust-lang.org/nightly/cargo/reference/config.html#httptimeout
666        // `UV_REQUEST_TIMEOUT` is provided for backwards compatibility with v0.1.6
667        let http_read_timeout = parse_integer_environment_variable(
668            EnvVars::UV_HTTP_TIMEOUT,
669            Some("value should be an integer number of seconds"),
670        )?
671        .or(parse_integer_environment_variable(
672            EnvVars::UV_REQUEST_TIMEOUT,
673            Some("value should be an integer number of seconds"),
674        )?)
675        .or(parse_integer_environment_variable(
676            EnvVars::HTTP_TIMEOUT,
677            Some("value should be an integer number of seconds"),
678        )?)
679        .map(Duration::from_secs);
680
681        Ok(Self {
682            skip_wheel_filename_check: parse_boolish_environment_variable(
683                EnvVars::UV_SKIP_WHEEL_FILENAME_CHECK,
684            )?,
685            hide_build_output: parse_boolish_environment_variable(EnvVars::UV_HIDE_BUILD_OUTPUT)?,
686            python_install_bin: parse_boolish_environment_variable(EnvVars::UV_PYTHON_INSTALL_BIN)?,
687            python_install_registry: parse_boolish_environment_variable(
688                EnvVars::UV_PYTHON_INSTALL_REGISTRY,
689            )?,
690            concurrency: Concurrency {
691                downloads: parse_integer_environment_variable(
692                    EnvVars::UV_CONCURRENT_DOWNLOADS,
693                    None,
694                )?,
695                builds: parse_integer_environment_variable(EnvVars::UV_CONCURRENT_BUILDS, None)?,
696                installs: parse_integer_environment_variable(
697                    EnvVars::UV_CONCURRENT_INSTALLS,
698                    None,
699                )?,
700            },
701            install_mirrors: PythonInstallMirrors {
702                python_install_mirror: parse_string_environment_variable(
703                    EnvVars::UV_PYTHON_INSTALL_MIRROR,
704                )?,
705                pypy_install_mirror: parse_string_environment_variable(
706                    EnvVars::UV_PYPY_INSTALL_MIRROR,
707                )?,
708                python_downloads_json_url: parse_string_environment_variable(
709                    EnvVars::UV_PYTHON_DOWNLOADS_JSON_URL,
710                )?,
711            },
712            log_context: parse_boolish_environment_variable(EnvVars::UV_LOG_CONTEXT)?,
713            lfs: parse_boolish_environment_variable(EnvVars::UV_GIT_LFS)?,
714            http_read_timeout_upload: parse_integer_environment_variable(
715                EnvVars::UV_UPLOAD_HTTP_TIMEOUT,
716                Some("value should be an integer number of seconds"),
717            )?
718            .map(Duration::from_secs)
719            .or(http_read_timeout)
720            .unwrap_or(DEFAULT_READ_TIMEOUT_UPLOAD),
721            http_read_timeout: http_read_timeout.unwrap_or(DEFAULT_READ_TIMEOUT),
722            http_connect_timeout: parse_integer_environment_variable(
723                EnvVars::UV_HTTP_CONNECT_TIMEOUT,
724                Some("value should be an integer number of seconds"),
725            )?
726            .map(Duration::from_secs)
727            .unwrap_or(DEFAULT_CONNECT_TIMEOUT),
728            http_retries: parse_integer_environment_variable(EnvVars::UV_HTTP_RETRIES, None)?
729                .unwrap_or(uv_client::DEFAULT_RETRIES),
730            #[cfg(feature = "tracing-durations-export")]
731            tracing_durations_file: parse_path_environment_variable(
732                EnvVars::TRACING_DURATIONS_FILE,
733            ),
734            frozen: EnvFlag::new(EnvVars::UV_FROZEN)?,
735            locked: EnvFlag::new(EnvVars::UV_LOCKED)?,
736            offline: EnvFlag::new(EnvVars::UV_OFFLINE)?,
737            no_sync: EnvFlag::new(EnvVars::UV_NO_SYNC)?,
738            managed_python: EnvFlag::new(EnvVars::UV_MANAGED_PYTHON)?,
739            no_managed_python: EnvFlag::new(EnvVars::UV_NO_MANAGED_PYTHON)?,
740            native_tls: EnvFlag::new(EnvVars::UV_NATIVE_TLS)?,
741            preview: EnvFlag::new(EnvVars::UV_PREVIEW)?,
742            isolated: EnvFlag::new(EnvVars::UV_ISOLATED)?,
743            no_progress: EnvFlag::new(EnvVars::UV_NO_PROGRESS)?,
744            no_installer_metadata: EnvFlag::new(EnvVars::UV_NO_INSTALLER_METADATA)?,
745            dev: EnvFlag::new(EnvVars::UV_DEV)?,
746            no_dev: EnvFlag::new(EnvVars::UV_NO_DEV)?,
747            show_resolution: EnvFlag::new(EnvVars::UV_SHOW_RESOLUTION)?,
748            no_editable: EnvFlag::new(EnvVars::UV_NO_EDITABLE)?,
749            no_env_file: EnvFlag::new(EnvVars::UV_NO_ENV_FILE)?,
750            venv_seed: EnvFlag::new(EnvVars::UV_VENV_SEED)?,
751            venv_clear: EnvFlag::new(EnvVars::UV_VENV_CLEAR)?,
752        })
753    }
754}
755
756/// Parse a string environment variable.
757fn parse_string_environment_variable(name: &'static str) -> Result<Option<String>, Error> {
758    match std::env::var(name) {
759        Ok(v) => {
760            if v.is_empty() {
761                Ok(None)
762            } else {
763                Ok(Some(v))
764            }
765        }
766        Err(e) => match e {
767            std::env::VarError::NotPresent => Ok(None),
768            std::env::VarError::NotUnicode(err) => Err(Error::InvalidEnvironmentVariable(
769                InvalidEnvironmentVariable {
770                    name: name.to_string(),
771                    value: err.to_string_lossy().to_string(),
772                    err: "expected a valid UTF-8 string".to_string(),
773                },
774            )),
775        },
776    }
777}
778
779fn parse_integer_environment_variable<T>(
780    name: &'static str,
781    help: Option<&str>,
782) -> Result<Option<T>, Error>
783where
784    T: std::str::FromStr + Copy,
785    <T as std::str::FromStr>::Err: std::fmt::Display,
786{
787    let value = match std::env::var(name) {
788        Ok(v) => v,
789        Err(e) => {
790            return match e {
791                std::env::VarError::NotPresent => Ok(None),
792                std::env::VarError::NotUnicode(err) => Err(Error::InvalidEnvironmentVariable(
793                    InvalidEnvironmentVariable {
794                        name: name.to_string(),
795                        value: err.to_string_lossy().to_string(),
796                        err: "expected a valid UTF-8 string".to_string(),
797                    },
798                )),
799            };
800        }
801    };
802    if value.is_empty() {
803        return Ok(None);
804    }
805
806    match value.parse::<T>() {
807        Ok(v) => Ok(Some(v)),
808        Err(err) => Err(Error::InvalidEnvironmentVariable(
809            InvalidEnvironmentVariable {
810                name: name.to_string(),
811                value,
812                err: if let Some(help) = help {
813                    format!("{err}; {help}")
814                } else {
815                    err.to_string()
816                },
817            },
818        )),
819    }
820}
821
822#[cfg(feature = "tracing-durations-export")]
823/// Parse a path environment variable.
824fn parse_path_environment_variable(name: &'static str) -> Option<PathBuf> {
825    let value = std::env::var_os(name)?;
826
827    if value.is_empty() {
828        return None;
829    }
830
831    Some(PathBuf::from(value))
832}
833
834/// Populate the [`EnvironmentFlags`] from the given [`EnvironmentOptions`].
835impl From<&EnvironmentOptions> for EnvironmentFlags {
836    fn from(options: &EnvironmentOptions) -> Self {
837        let mut flags = Self::empty();
838        if options.skip_wheel_filename_check == Some(true) {
839            flags.insert(Self::SKIP_WHEEL_FILENAME_CHECK);
840        }
841        if options.hide_build_output == Some(true) {
842            flags.insert(Self::HIDE_BUILD_OUTPUT);
843        }
844        flags
845    }
846}