Skip to content

Commit cd55d1c

Browse files
Manually parse and reconcile Boolean environment variables (#17321)
## Summary This gives us more flexibility since we can avoid erroring on "conflicts" when one option is disabled (e.g., `UV_FROZEN=0 uv lock --check`). Closes #13385. Closes #13316. --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent e67dbce commit cd55d1c

File tree

19 files changed

+1037
-342
lines changed

19 files changed

+1037
-342
lines changed

clippy.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ doc-valid-idents = [
99
"ROCm",
1010
"XPU",
1111
"PowerShell",
12+
"UV_DEV",
13+
"UV_FROZEN",
14+
"UV_ISOLATED",
15+
"UV_LOCKED",
16+
"UV_MANAGED_PYTHON",
17+
"UV_NATIVE_TLS",
18+
"UV_NO_DEV",
19+
"UV_NO_EDITABLE",
20+
"UV_NO_ENV_FILE",
21+
"UV_NO_INSTALLER_METADATA",
22+
"UV_NO_MANAGED_PYTHON",
23+
"UV_NO_PROGRESS",
24+
"UV_NO_SYNC",
25+
"UV_OFFLINE",
26+
"UV_PREVIEW",
27+
"UV_SHOW_RESOLUTION",
28+
"UV_VENV_CLEAR",
29+
"UV_VENV_SEED",
1230
".." # Include the defaults
1331
]
1432

crates/uv-cli/src/lib.rs

Lines changed: 108 additions & 115 deletions
Large diffs are not rendered by default.

crates/uv-cli/src/options.rs

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
use std::fmt;
2+
13
use anstream::eprintln;
24

35
use uv_cache::Refresh;
46
use uv_configuration::{BuildIsolation, Reinstall, Upgrade};
57
use uv_distribution_types::{ConfigSettings, PackageConfigSettings, Requirement};
68
use uv_resolver::{ExcludeNewer, ExcludeNewerPackage, PrereleaseMode};
7-
use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions};
9+
use uv_settings::{Combine, EnvFlag, PipOptions, ResolverInstallerOptions, ResolverOptions};
810
use uv_warnings::owo_colors::OwoColorize;
911

1012
use crate::{
@@ -37,6 +39,150 @@ pub fn flag(yes: bool, no: bool, name: &str) -> Option<bool> {
3739
}
3840
}
3941

42+
/// The source of a boolean flag value.
43+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44+
pub enum FlagSource {
45+
/// The flag was set via command-line argument.
46+
Cli,
47+
/// The flag was set via environment variable.
48+
Env(&'static str),
49+
/// The flag was set via workspace/project configuration.
50+
Config,
51+
}
52+
53+
impl fmt::Display for FlagSource {
54+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55+
match self {
56+
Self::Cli => write!(f, "command-line argument"),
57+
Self::Env(name) => write!(f, "environment variable `{name}`"),
58+
Self::Config => write!(f, "workspace configuration"),
59+
}
60+
}
61+
}
62+
63+
/// A boolean flag value with its source.
64+
#[derive(Debug, Clone, Copy)]
65+
pub enum Flag {
66+
/// The flag is not set.
67+
Disabled,
68+
/// The flag is enabled with a known source.
69+
Enabled {
70+
source: FlagSource,
71+
/// The CLI flag name (e.g., "locked" for `--locked`).
72+
name: &'static str,
73+
},
74+
}
75+
76+
impl Flag {
77+
/// Create a flag that is explicitly disabled.
78+
pub const fn disabled() -> Self {
79+
Self::Disabled
80+
}
81+
82+
/// Create an enabled flag from a CLI argument.
83+
pub const fn from_cli(name: &'static str) -> Self {
84+
Self::Enabled {
85+
source: FlagSource::Cli,
86+
name,
87+
}
88+
}
89+
90+
/// Create an enabled flag from workspace/project configuration.
91+
pub const fn from_config(name: &'static str) -> Self {
92+
Self::Enabled {
93+
source: FlagSource::Config,
94+
name,
95+
}
96+
}
97+
98+
/// Returns `true` if the flag is set.
99+
pub fn is_enabled(self) -> bool {
100+
matches!(self, Self::Enabled { .. })
101+
}
102+
103+
/// Returns the source of the flag, if it is set.
104+
pub fn source(self) -> Option<FlagSource> {
105+
match self {
106+
Self::Disabled => None,
107+
Self::Enabled { source, .. } => Some(source),
108+
}
109+
}
110+
111+
/// Returns the CLI flag name, if the flag is enabled.
112+
pub fn name(self) -> Option<&'static str> {
113+
match self {
114+
Self::Disabled => None,
115+
Self::Enabled { name, .. } => Some(name),
116+
}
117+
}
118+
}
119+
120+
impl From<Flag> for bool {
121+
fn from(flag: Flag) -> Self {
122+
flag.is_enabled()
123+
}
124+
}
125+
126+
/// Resolve a boolean flag from CLI arguments and an environment variable.
127+
///
128+
/// The CLI argument takes precedence over the environment variable. Returns a [`Flag`] with the
129+
/// resolved value and source.
130+
pub fn resolve_flag(cli_flag: bool, name: &'static str, env_flag: EnvFlag) -> Flag {
131+
if cli_flag {
132+
Flag::Enabled {
133+
source: FlagSource::Cli,
134+
name,
135+
}
136+
} else if env_flag.value == Some(true) {
137+
Flag::Enabled {
138+
source: FlagSource::Env(env_flag.env_var),
139+
name,
140+
}
141+
} else {
142+
Flag::Disabled
143+
}
144+
}
145+
146+
/// Check if two flags conflict and exit with an error if they do.
147+
///
148+
/// This function checks if both flags are enabled (truthy) and reports an error if so, including
149+
/// the source of each flag (CLI or environment variable) in the error message.
150+
pub fn check_conflicts(flag_a: Flag, flag_b: Flag) {
151+
if let (
152+
Flag::Enabled {
153+
source: source_a,
154+
name: name_a,
155+
},
156+
Flag::Enabled {
157+
source: source_b,
158+
name: name_b,
159+
},
160+
) = (flag_a, flag_b)
161+
{
162+
let display_a = match source_a {
163+
FlagSource::Cli => format!("`--{name_a}`"),
164+
FlagSource::Env(env) => format!("`{env}` (environment variable)"),
165+
FlagSource::Config => format!("`{name_a}` (workspace configuration)"),
166+
};
167+
let display_b = match source_b {
168+
FlagSource::Cli => format!("`--{name_b}`"),
169+
FlagSource::Env(env) => format!("`{env}` (environment variable)"),
170+
FlagSource::Config => format!("`{name_b}` (workspace configuration)"),
171+
};
172+
eprintln!(
173+
"{}{} the argument {} cannot be used with {}",
174+
"error".bold().red(),
175+
":".bold(),
176+
display_a.green(),
177+
display_b.green(),
178+
);
179+
#[allow(clippy::exit)]
180+
{
181+
std::process::exit(2);
182+
}
183+
}
184+
}
185+
40186
impl From<RefreshArgs> for Refresh {
41187
fn from(value: RefreshArgs) -> Self {
42188
let RefreshArgs {

crates/uv-settings/src/lib.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,25 @@ pub struct Concurrency {
594594
pub installs: Option<NonZeroUsize>,
595595
}
596596

597+
/// A boolean flag parsed from an environment variable.
598+
///
599+
/// Stores both the value and the environment variable name for use in error messages.
600+
#[derive(Debug, Clone, Copy)]
601+
pub struct EnvFlag {
602+
pub value: Option<bool>,
603+
pub env_var: &'static str,
604+
}
605+
606+
impl EnvFlag {
607+
/// Create a new [`EnvFlag`] by parsing the given environment variable.
608+
pub fn new(env_var: &'static str) -> Result<Self, Error> {
609+
Ok(Self {
610+
value: parse_boolish_environment_variable(env_var)?,
611+
env_var,
612+
})
613+
}
614+
}
615+
597616
/// Options loaded from environment variables.
598617
///
599618
/// This is currently a subset of all respected environment variables, most are parsed via Clap at
@@ -613,6 +632,24 @@ pub struct EnvironmentOptions {
613632
pub concurrency: Concurrency,
614633
#[cfg(feature = "tracing-durations-export")]
615634
pub tracing_durations_file: Option<PathBuf>,
635+
pub frozen: EnvFlag,
636+
pub locked: EnvFlag,
637+
pub offline: EnvFlag,
638+
pub no_sync: EnvFlag,
639+
pub managed_python: EnvFlag,
640+
pub no_managed_python: EnvFlag,
641+
pub native_tls: EnvFlag,
642+
pub preview: EnvFlag,
643+
pub isolated: EnvFlag,
644+
pub no_progress: EnvFlag,
645+
pub no_installer_metadata: EnvFlag,
646+
pub dev: EnvFlag,
647+
pub no_dev: EnvFlag,
648+
pub show_resolution: EnvFlag,
649+
pub no_editable: EnvFlag,
650+
pub no_env_file: EnvFlag,
651+
pub venv_seed: EnvFlag,
652+
pub venv_clear: EnvFlag,
616653
}
617654

618655
impl EnvironmentOptions {
@@ -667,6 +704,24 @@ impl EnvironmentOptions {
667704
tracing_durations_file: parse_path_environment_variable(
668705
EnvVars::TRACING_DURATIONS_FILE,
669706
),
707+
frozen: EnvFlag::new(EnvVars::UV_FROZEN)?,
708+
locked: EnvFlag::new(EnvVars::UV_LOCKED)?,
709+
offline: EnvFlag::new(EnvVars::UV_OFFLINE)?,
710+
no_sync: EnvFlag::new(EnvVars::UV_NO_SYNC)?,
711+
managed_python: EnvFlag::new(EnvVars::UV_MANAGED_PYTHON)?,
712+
no_managed_python: EnvFlag::new(EnvVars::UV_NO_MANAGED_PYTHON)?,
713+
native_tls: EnvFlag::new(EnvVars::UV_NATIVE_TLS)?,
714+
preview: EnvFlag::new(EnvVars::UV_PREVIEW)?,
715+
isolated: EnvFlag::new(EnvVars::UV_ISOLATED)?,
716+
no_progress: EnvFlag::new(EnvVars::UV_NO_PROGRESS)?,
717+
no_installer_metadata: EnvFlag::new(EnvVars::UV_NO_INSTALLER_METADATA)?,
718+
dev: EnvFlag::new(EnvVars::UV_DEV)?,
719+
no_dev: EnvFlag::new(EnvVars::UV_NO_DEV)?,
720+
show_resolution: EnvFlag::new(EnvVars::UV_SHOW_RESOLUTION)?,
721+
no_editable: EnvFlag::new(EnvVars::UV_NO_EDITABLE)?,
722+
no_env_file: EnvFlag::new(EnvVars::UV_NO_ENV_FILE)?,
723+
venv_seed: EnvFlag::new(EnvVars::UV_VENV_SEED)?,
724+
venv_clear: EnvFlag::new(EnvVars::UV_VENV_CLEAR)?,
670725
})
671726
}
672727
}

0 commit comments

Comments
 (0)