Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions clippy.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
91 changes: 90 additions & 1 deletion crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Item = &'a PathBuf>,
) -> anyhow::Result<Vec<(String, String)>> {
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<ExitStatus> for ExitCode {
fn from(status: ExitStatus) -> Self {
match status {
Expand Down
39 changes: 3 additions & 36 deletions crates/uv/src/commands/project/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could also ban this API via clippy.toml? Along with the other in-place env mutation APIs 🙂

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);
Expand Down Expand Up @@ -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(
Expand Down
103 changes: 6 additions & 97 deletions crates/uv/src/commands/tool/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -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<Vec<(String, String)>> {
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| {
Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions crates/uv/tests/it/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
}

Expand Down
Loading