Skip to main content

uv_settings/
lib.rs

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