From 8e53364f41b5aaab373a4a92480a2639449404fa Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 26 May 2026 17:05:25 +0200 Subject: [PATCH 1/2] Avoid modifying process with --env-file in uv run --- crates/uv/src/commands/mod.rs | 91 ++++++++++++++++++++++- crates/uv/src/commands/project/run.rs | 39 +--------- crates/uv/src/commands/tool/run.rs | 103 ++------------------------ crates/uv/tests/it/run.rs | 29 ++++++++ 4 files changed, 128 insertions(+), 134 deletions(-) diff --git a/crates/uv/src/commands/mod.rs b/crates/uv/src/commands/mod.rs index 62df3cc29c79f..dd8e13d17c076 100644 --- a/crates/uv/src/commands/mod.rs +++ b/crates/uv/src/commands/mod.rs @@ -5,9 +5,10 @@ use std::time::Duration; use std::{fmt::Write, process::ExitCode}; use anstream::AutoStream; -use anyhow::Context; +use anyhow::{Context, bail}; use owo_colors::OwoColorize; use tracing::debug; +use uv_warnings::warn_user; pub(crate) use auth::dir::dir as auth_dir; pub(crate) use auth::helper::helper as auth_helper; @@ -112,6 +113,94 @@ pub(crate) enum ExitStatus { External(u8), } +/// Read dotenv files into an overlay for a spawned process. +/// +/// These values intentionally do not mutate uv's process environment and cannot mutate +/// the current uv process' settings. +fn read_env_files<'a>( + env_file: impl DoubleEndedIterator, +) -> anyhow::Result> { + let mut environment = Vec::new(); + + for env_file_path in env_file.rev().map(PathBuf::as_path) { + let iter = match dotenvy::from_path_iter(env_file_path) { + Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { + bail!( + "No environment file found at: `{}`", + env_file_path.simplified_display() + ); + } + Err(dotenvy::Error::Io(err)) => { + bail!( + "Failed to read environment file `{}`: {err}", + env_file_path.simplified_display() + ); + } + Err(dotenvy::Error::LineParse(content, position)) => { + warn_user!( + "Failed to parse environment file `{}` at position {position}: {content}", + env_file_path.simplified_display(), + ); + continue; + } + Err(err) => { + warn_user!( + "Failed to parse environment file `{}`: {err}", + env_file_path.simplified_display(), + ); + continue; + } + Ok(iter) => iter, + }; + + let mut parsed = true; + for item in iter { + match item { + Ok((key, value)) => { + if std::env::var(&key).is_err() { + environment.push((key, value)); + } + } + Err(dotenvy::Error::Io(err)) => { + bail!( + "Failed to read environment file `{}`: {err}", + env_file_path.simplified_display() + ); + } + Err(dotenvy::Error::LineParse(content, position)) => { + warn_user!( + "Failed to parse environment file `{}` at position {position}: {content}", + env_file_path.simplified_display(), + ); + parsed = false; + break; + } + Err(err) => { + warn_user!( + "Failed to parse environment file `{}`: {err}", + env_file_path.simplified_display(), + ); + parsed = false; + break; + } + } + } + + if parsed { + debug!( + "Read environment file at: `{}`", + env_file_path.simplified_display() + ); + } + } + + // `dotenvy::from_path` preserves the first loaded value, while `Command::envs` preserves the + // last value set for the child process. + environment.reverse(); + + Ok(environment) +} + impl From for ExitCode { fn from(status: ExitStatus) -> Self { match status { diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 7d43ae3c0e930..2111c91dc6902 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -74,7 +74,7 @@ use crate::commands::project::{ update_environment, validate_project_requires_python, }; use crate::commands::reporters::PythonDownloadReporter; -use crate::commands::{ExitStatus, diagnostics, project}; +use crate::commands::{ExitStatus, diagnostics, project, read_env_files}; use crate::printer::Printer; use crate::settings::{ FrozenSource, GlobalSettings, LockCheck, LockCheckSource, ResolverInstallerSettings, @@ -165,41 +165,7 @@ pub(crate) async fn run( let lock_state = UniversalState::default(); let sync_state = lock_state.fork(); - // Read from the `.env` file, if necessary. - for env_file_path in env_file.iter().rev().map(PathBuf::as_path) { - match dotenvy::from_path(env_file_path) { - Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { - bail!( - "No environment file found at: `{}`", - env_file_path.simplified_display() - ); - } - Err(dotenvy::Error::Io(err)) => { - bail!( - "Failed to read environment file `{}`: {err}", - env_file_path.simplified_display() - ); - } - Err(dotenvy::Error::LineParse(content, position)) => { - warn_user!( - "Failed to parse environment file `{}` at position {position}: {content}", - env_file_path.simplified_display(), - ); - } - Err(err) => { - warn_user!( - "Failed to parse environment file `{}`: {err}", - env_file_path.simplified_display(), - ); - } - Ok(()) => { - debug!( - "Read environment file at: `{}`", - env_file_path.simplified_display() - ); - } - } - } + let env_file_environment = read_env_files(env_file.iter())?; // Initialize any output reporters. let download_reporter = PythonDownloadReporter::single(printer); @@ -1298,6 +1264,7 @@ pub(crate) async fn run( debug!("Running `{command}`"); let mut process = command.as_command(interpreter); + process.envs(env_file_environment); // Construct the `PATH` environment variable. let new_path = std::env::join_paths( diff --git a/crates/uv/src/commands/tool/run.rs b/crates/uv/src/commands/tool/run.rs index a81d92ec61352..bb4f331da787e 100644 --- a/crates/uv/src/commands/tool/run.rs +++ b/crates/uv/src/commands/tool/run.rs @@ -24,7 +24,6 @@ use uv_distribution_types::{ RequirementSource, UnresolvedRequirement, UnresolvedRequirementSpecification, }; use uv_fs::CWD; -use uv_fs::Simplified; use uv_installer::{InstallationStrategy, SatisfiesResult, SitePackages}; use uv_normalize::PackageName; use uv_pep440::{VersionSpecifier, VersionSpecifiers}; @@ -41,7 +40,6 @@ use uv_settings::{PythonInstallMirrors, ResolverInstallerOptions, ToolOptions}; use uv_shell::WindowsRunnable; use uv_static::EnvVars; use uv_tool::{InstalledTools, entrypoint_paths}; -use uv_warnings::warn_user; use uv_warnings::warn_user_once; use uv_workspace::WorkspaceCache; @@ -60,7 +58,7 @@ use crate::commands::project::{ use crate::commands::reporters::PythonDownloadReporter; use crate::commands::tool::common::{matching_packages, refine_interpreter}; use crate::commands::tool::{Target, ToolRequest}; -use crate::commands::{diagnostics, project::environment::CachedEnvironment}; +use crate::commands::{diagnostics, project::environment::CachedEnvironment, read_env_files}; use crate::printer::Printer; use crate::settings::ResolverInstallerSettings; use crate::settings::ResolverSettings; @@ -83,99 +81,6 @@ impl Display for ToolRunCommand { } } -/// Read dotenv files into an overlay for the spawned tool process. -/// -/// These values intentionally do not mutate uv's process environment and cannot mutate -/// the current uv process' settings. -fn read_env_files( - env_file: &[PathBuf], - no_env_file: bool, -) -> anyhow::Result> { - let mut environment = Vec::new(); - - if no_env_file { - return Ok(environment); - } - - for env_file_path in env_file.iter().rev().map(PathBuf::as_path) { - let iter = match dotenvy::from_path_iter(env_file_path) { - Err(dotenvy::Error::Io(err)) if err.kind() == std::io::ErrorKind::NotFound => { - bail!( - "No environment file found at: `{}`", - env_file_path.simplified_display() - ); - } - Err(dotenvy::Error::Io(err)) => { - bail!( - "Failed to read environment file `{}`: {err}", - env_file_path.simplified_display() - ); - } - Err(dotenvy::Error::LineParse(content, position)) => { - warn_user!( - "Failed to parse environment file `{}` at position {position}: {content}", - env_file_path.simplified_display(), - ); - continue; - } - Err(err) => { - warn_user!( - "Failed to parse environment file `{}`: {err}", - env_file_path.simplified_display(), - ); - continue; - } - Ok(iter) => iter, - }; - - let mut parsed = true; - for item in iter { - match item { - Ok((key, value)) => { - if std::env::var(&key).is_err() { - environment.push((key, value)); - } - } - Err(dotenvy::Error::Io(err)) => { - bail!( - "Failed to read environment file `{}`: {err}", - env_file_path.simplified_display() - ); - } - Err(dotenvy::Error::LineParse(content, position)) => { - warn_user!( - "Failed to parse environment file `{}` at position {position}: {content}", - env_file_path.simplified_display(), - ); - parsed = false; - break; - } - Err(err) => { - warn_user!( - "Failed to parse environment file `{}`: {err}", - env_file_path.simplified_display(), - ); - parsed = false; - break; - } - } - } - - if parsed { - debug!( - "Read environment file at: `{}`", - env_file_path.simplified_display() - ); - } - } - - // `dotenvy::from_path` preserves the first loaded value, while `Command::envs` preserves the - // last value set for the child process. - environment.reverse(); - - Ok(environment) -} - /// Check if the given arguments contain a verbose flag (e.g., `--verbose`, `-v`, `-vv`, etc.) fn find_verbose_flag(args: &[std::ffi::OsString]) -> Option<&str> { args.iter().find_map(|arg| { @@ -232,7 +137,11 @@ pub(crate) async fn run( ); } - let env_file_environment = read_env_files(&env_file, no_env_file)?; + let env_file_environment = if no_env_file { + Vec::new() + } else { + read_env_files(env_file.iter())? + }; let Some(command) = command else { // When a command isn't provided, we'll show a brief help including available tools diff --git a/crates/uv/tests/it/run.rs b/crates/uv/tests/it/run.rs index a642bb47cd219..489c48930f992 100644 --- a/crates/uv/tests/it/run.rs +++ b/crates/uv/tests/it/run.rs @@ -5082,6 +5082,35 @@ fn run_with_env_file() -> Result<()> { ----- stderr ----- "); + context.temp_dir.child(".file").write_str(indoc! { " + UV_PYTHON_SEARCH_PATH=.no-python + THE_EMPIRE_VARIABLE=palpatine + REBEL_1=leia_organa + REBEL_2=obi_wan_kenobi + REBEL_3=C3PO + " + })?; + + uv_snapshot!(context.filters(), context.run() + .arg("--no-project") + .arg("--no-managed-python") + .arg("--python").arg("3.12") + .arg("--env-file").arg(".file") + .arg("test.py") + .env_remove(EnvVars::VIRTUAL_ENV) + .env_remove(EnvVars::UV_PYTHON_SEARCH_PATH) + .env(EnvVars::PATH, context.python_path()), @" + success: true + exit_code: 0 + ----- stdout ----- + palpatine + leia_organa + obi_wan_kenobi + C3PO + + ----- stderr ----- + "); + Ok(()) } From 212af333ca19a09fff914d3bf17c81de2a7356c6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 26 May 2026 17:41:31 +0200 Subject: [PATCH 2/2] Ban calls --- clippy.toml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/clippy.toml b/clippy.toml index 8171ad154cbcb..2a018c7cd877e 100644 --- a/clippy.toml +++ b/clippy.toml @@ -47,6 +47,18 @@ disallowed-types = [ ] disallowed-methods = [ + "dotenvy::dotenv", + "dotenvy::dotenv_override", + "dotenvy::from_filename", + "dotenvy::from_filename_override", + "dotenvy::from_path", + "dotenvy::from_path_override", + "dotenvy::from_read", + "dotenvy::from_read_override", + "dotenvy::var", + "dotenvy::vars", + "dotenvy::Iter::load", + "dotenvy::Iter::load_override", "std::fs::canonicalize", "std::fs::copy", "std::fs::create_dir",