uv/crates/uv/src/commands/project/run.rs

1628 lines
61 KiB
Rust

use std::borrow::Cow;
use std::env::VarError;
use std::ffi::OsString;
use std::fmt::Write;
use std::io::Read;
use std::path::{Path, PathBuf};
use anyhow::{Context, anyhow, bail};
use futures::StreamExt;
use itertools::Itertools;
use owo_colors::OwoColorize;
use tokio::process::Command;
use tracing::{debug, warn};
use url::Url;
use uv_cache::Cache;
use uv_cli::ExternalCommand;
use uv_client::BaseClientBuilder;
use uv_configuration::{
Concurrency, Constraints, DependencyGroups, DryRun, EditableMode, ExtrasSpecification,
InstallOptions, PreviewMode,
};
use uv_distribution_types::Requirement;
use uv_fs::which::is_executable;
use uv_fs::{PythonExt, Simplified};
use uv_installer::{SatisfiesResult, SitePackages};
use uv_normalize::{DefaultExtras, DefaultGroups, PackageName};
use uv_python::{
EnvironmentPreference, Interpreter, PyVenvConfiguration, PythonDownloads, PythonEnvironment,
PythonInstallation, PythonPreference, PythonRequest, PythonVersionFile,
VersionFileDiscoveryOptions,
};
use uv_redacted::DisplaySafeUrl;
use uv_requirements::{RequirementsSource, RequirementsSpecification};
use uv_resolver::{Installable, Lock, Preference};
use uv_scripts::Pep723Item;
use uv_settings::PythonInstallMirrors;
use uv_shell::runnable::WindowsRunnable;
use uv_static::EnvVars;
use uv_warnings::warn_user;
use uv_workspace::{DiscoveryOptions, VirtualProject, Workspace, WorkspaceCache, WorkspaceError};
use crate::commands::pip::loggers::{
DefaultInstallLogger, DefaultResolveLogger, SummaryInstallLogger, SummaryResolveLogger,
};
use crate::commands::pip::operations::Modifications;
use crate::commands::project::environment::CachedEnvironment;
use crate::commands::project::install_target::InstallTarget;
use crate::commands::project::lock::LockMode;
use crate::commands::project::lock_target::LockTarget;
use crate::commands::project::{
EnvironmentSpecification, PreferenceLocation, ProjectEnvironment, ProjectError,
ScriptEnvironment, ScriptInterpreter, UniversalState, WorkspacePython,
default_dependency_groups, script_specification, update_environment,
validate_project_requires_python,
};
use crate::commands::reporters::PythonDownloadReporter;
use crate::commands::run::run_to_completion;
use crate::commands::{ExitStatus, diagnostics, project};
use crate::printer::Printer;
use crate::settings::{NetworkSettings, ResolverInstallerSettings};
/// Run a command.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn run(
project_dir: &Path,
script: Option<Pep723Item>,
command: Option<RunCommand>,
requirements: Vec<RequirementsSource>,
show_resolution: bool,
locked: bool,
frozen: bool,
active: Option<bool>,
no_sync: bool,
isolated: bool,
all_packages: bool,
package: Option<PackageName>,
no_project: bool,
no_config: bool,
extras: ExtrasSpecification,
groups: DependencyGroups,
editable: EditableMode,
modifications: Modifications,
python: Option<String>,
install_mirrors: PythonInstallMirrors,
settings: ResolverInstallerSettings,
network_settings: NetworkSettings,
python_preference: PythonPreference,
python_downloads: PythonDownloads,
installer_metadata: bool,
concurrency: Concurrency,
cache: &Cache,
printer: Printer,
env_file: Vec<PathBuf>,
no_env_file: bool,
preview: PreviewMode,
max_recursion_depth: u32,
) -> anyhow::Result<ExitStatus> {
// Check if max recursion depth was exceeded. This most commonly happens
// for scripts with a shebang line like `#!/usr/bin/env -S uv run`, so try
// to provide guidance for that case.
let recursion_depth = read_recursion_depth_from_environment_variable()?;
if recursion_depth > max_recursion_depth {
bail!(
r"
`uv run` was recursively invoked {recursion_depth} times which exceeds the limit of {max_recursion_depth}.
hint: If you are running a script with `{}` in the shebang, you may need to include the `{}` flag.",
"uv run".green(),
"--script".green(),
);
}
// These cases seem quite complex because (in theory) they should change the "current package".
// Let's ban them entirely for now.
let mut requirements_from_stdin: bool = false;
for source in &requirements {
match source {
RequirementsSource::PyprojectToml(_) => {
bail!("Adding requirements from a `pyproject.toml` is not supported in `uv run`");
}
RequirementsSource::SetupPy(_) => {
bail!("Adding requirements from a `setup.py` is not supported in `uv run`");
}
RequirementsSource::SetupCfg(_) => {
bail!("Adding requirements from a `setup.cfg` is not supported in `uv run`");
}
RequirementsSource::RequirementsTxt(path) => {
if path == Path::new("-") {
requirements_from_stdin = true;
}
}
_ => {}
}
}
// Fail early if stdin is used for multiple purposes.
if matches!(
command,
Some(RunCommand::PythonStdin(..) | RunCommand::PythonGuiStdin(..))
) && requirements_from_stdin
{
bail!("Cannot read both requirements file and script from stdin");
}
// Initialize any shared state.
let lock_state = UniversalState::default();
let sync_state = lock_state.fork();
let workspace_cache = WorkspaceCache::default();
// Read from the `.env` file, if necessary.
if !no_env_file {
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()
);
}
}
}
}
// Initialize any output reporters.
let download_reporter = PythonDownloadReporter::single(printer);
// The lockfile used for the base environment.
let mut base_lock: Option<(Lock, PathBuf)> = None;
// Determine whether the command to execute is a PEP 723 script.
let temp_dir;
let script_interpreter = if let Some(script) = script {
match &script {
Pep723Item::Script(script) => {
debug!(
"Reading inline script metadata from `{}`",
script.path.user_display()
);
}
Pep723Item::Stdin(..) => {
if requirements_from_stdin {
bail!("Cannot read both requirements file and script from stdin");
}
debug!("Reading inline script metadata from stdin");
}
Pep723Item::Remote(..) => {
debug!("Reading inline script metadata from remote URL");
}
}
// If a lockfile already exists, lock the script.
if let Some(target) = script
.as_script()
.map(LockTarget::from)
.filter(|target| target.lock_path().is_file())
{
debug!("Found existing lockfile for script");
// Discover the interpreter for the script.
let environment = ScriptEnvironment::get_or_init(
(&script).into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
python_downloads,
&install_mirrors,
no_sync,
no_config,
active.map_or(Some(false), Some),
cache,
DryRun::Disabled,
printer,
preview,
)
.await?
.into_environment()?;
let _lock = environment
.lock()
.await
.inspect_err(|err| {
warn!("Failed to acquire environment lock: {err}");
})
.ok();
// Determine the lock mode.
let mode = if frozen {
LockMode::Frozen
} else if locked {
LockMode::Locked(environment.interpreter())
} else {
LockMode::Write(environment.interpreter())
};
// Generate a lockfile.
let lock = match project::lock::LockOperation::new(
mode,
&settings.resolver,
&network_settings,
&lock_state,
if show_resolution {
Box::new(DefaultResolveLogger)
} else {
Box::new(SummaryResolveLogger)
},
concurrency,
cache,
&workspace_cache,
printer,
preview,
)
.execute(target)
.await
{
Ok(result) => result.into_lock(),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
network_settings.native_tls,
)
.with_context("script")
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
// Sync the environment.
let target = InstallTarget::Script {
script: script.as_script().unwrap(),
lock: &lock,
};
let install_options = InstallOptions::default();
match project::sync::do_sync(
target,
&environment,
&extras.with_defaults(DefaultExtras::default()),
&groups.with_defaults(DefaultGroups::default()),
editable,
install_options,
modifications,
None,
(&settings).into(),
&network_settings,
&sync_state,
if show_resolution {
Box::new(DefaultInstallLogger)
} else {
Box::new(SummaryInstallLogger)
},
installer_metadata,
concurrency,
cache,
workspace_cache.clone(),
DryRun::Disabled,
printer,
preview,
)
.await
{
Ok(()) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
network_settings.native_tls,
)
.with_context("script")
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
}
// Respect any locked preferences when resolving `--with` dependencies downstream.
let install_path = target.install_path().to_path_buf();
base_lock = Some((lock, install_path));
Some(environment.into_interpreter())
} else {
// If no lockfile is found, warn against `--locked` and `--frozen`.
if locked {
warn_user!(
"No lockfile found for Python script (ignoring `--locked`); run `{}` to generate a lockfile",
"uv lock --script".green(),
);
}
if frozen {
warn_user!(
"No lockfile found for Python script (ignoring `--frozen`); run `{}` to generate a lockfile",
"uv lock --script".green(),
);
}
// Install the script requirements, if necessary. Otherwise, use an isolated environment.
if let Some(spec) = script_specification((&script).into(), &settings.resolver)? {
let environment = ScriptEnvironment::get_or_init(
(&script).into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
python_downloads,
&install_mirrors,
no_sync,
no_config,
active.map_or(Some(false), Some),
cache,
DryRun::Disabled,
printer,
preview,
)
.await?
.into_environment()?;
let build_constraints = script
.metadata()
.tool
.as_ref()
.and_then(|tool| {
tool.uv
.as_ref()
.and_then(|uv| uv.build_constraint_dependencies.as_ref())
})
.map(|constraints| {
Constraints::from_requirements(
constraints
.iter()
.map(|constraint| Requirement::from(constraint.clone())),
)
});
let _lock = environment
.lock()
.await
.inspect_err(|err| {
warn!("Failed to acquire environment lock: {err}");
})
.ok();
match update_environment(
environment,
spec,
modifications,
build_constraints.unwrap_or_default(),
&settings,
&network_settings,
&sync_state,
if show_resolution {
Box::new(DefaultResolveLogger)
} else {
Box::new(SummaryResolveLogger)
},
if show_resolution {
Box::new(DefaultInstallLogger)
} else {
Box::new(SummaryInstallLogger)
},
installer_metadata,
concurrency,
cache,
workspace_cache.clone(),
DryRun::Disabled,
printer,
preview,
)
.await
{
Ok(update) => Some(update.into_environment().into_interpreter()),
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
network_settings.native_tls,
)
.with_context("script")
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
}
} else {
// Create a virtual environment.
let interpreter = ScriptInterpreter::discover(
(&script).into(),
python.as_deref().map(PythonRequest::parse),
&network_settings,
python_preference,
python_downloads,
&install_mirrors,
no_sync,
no_config,
active.map_or(Some(false), Some),
cache,
printer,
preview,
)
.await?
.into_interpreter();
temp_dir = cache.venv_dir()?;
let environment = uv_virtualenv::create_venv(
temp_dir.path(),
interpreter,
uv_virtualenv::Prompt::None,
false,
false,
false,
false,
false,
preview,
)?;
Some(environment.into_interpreter())
}
}
} else {
None
};
// Discover and sync the base environment.
let temp_dir;
let base_interpreter = if let Some(script_interpreter) = script_interpreter {
// If we found a PEP 723 script and the user provided a project-only setting, warn.
if no_project {
debug!(
"`--no-project` is a no-op for Python scripts with inline metadata; ignoring..."
);
}
if !extras.is_empty() {
warn_user!("Extras are not supported for Python scripts with inline metadata");
}
for flag in groups.history().as_flags_pretty() {
warn_user!("`{flag}` is not supported for Python scripts with inline metadata");
}
if all_packages {
warn_user!(
"`--all-packages` is a no-op for Python scripts with inline metadata, which always run in isolation"
);
}
if package.is_some() {
warn_user!(
"`--package` is a no-op for Python scripts with inline metadata, which always run in isolation"
);
}
if no_sync {
warn_user!(
"`--no-sync` is a no-op for Python scripts with inline metadata, which always run in isolation"
);
}
if isolated {
warn_user!(
"`--isolated` is a no-op for Python scripts with inline metadata, which always run in isolation"
);
}
script_interpreter
} else {
let project = if let Some(package) = package.as_ref() {
// We need a workspace, but we don't need to have a current package, we can be e.g. in
// the root of a virtual workspace and then switch into the selected package.
Some(VirtualProject::Project(
Workspace::discover(project_dir, &DiscoveryOptions::default(), &workspace_cache)
.await?
.with_current_project(package.clone())
.with_context(|| format!("Package `{package}` not found in workspace"))?,
))
} else {
match VirtualProject::discover(
project_dir,
&DiscoveryOptions::default(),
&workspace_cache,
)
.await
{
Ok(project) => {
if no_project {
debug!("Ignoring discovered project due to `--no-project`");
None
} else {
Some(project)
}
}
Err(WorkspaceError::MissingPyprojectToml | WorkspaceError::NonWorkspace(_)) => {
// If the user runs with `--no-project` and we can't find a project, warn.
if no_project {
warn!("`--no-project` was provided, but no project was found");
}
None
}
Err(err) => {
// If the user runs with `--no-project`, ignore the error.
if no_project {
warn!("Ignoring project discovery error due to `--no-project`: {err}");
None
} else {
return Err(err.into());
}
}
}
};
if no_project {
// If the user ran with `--no-project` and provided a project-only setting, warn.
for flag in extras.history().as_flags_pretty() {
warn_user!("`{flag}` has no effect when used alongside `--no-project`");
}
for flag in groups.history().as_flags_pretty() {
warn_user!("`{flag}` has no effect when used alongside `--no-project`");
}
if locked {
warn_user!("`--locked` has no effect when used alongside `--no-project`");
}
if frozen {
warn_user!("`--frozen` has no effect when used alongside `--no-project`");
}
if no_sync {
warn_user!("`--no-sync` has no effect when used alongside `--no-project`");
}
} else if project.is_none() {
// If we can't find a project and the user provided a project-only setting, warn.
for flag in extras.history().as_flags_pretty() {
warn_user!("`{flag}` has no effect when used outside of a project");
}
for flag in groups.history().as_flags_pretty() {
warn_user!("`{flag}` has no effect when used outside of a project");
}
if locked {
warn_user!("`--locked` has no effect when used outside of a project");
}
if no_sync {
warn_user!("`--no-sync` has no effect when used outside of a project");
}
}
if let Some(project) = project {
if let Some(project_name) = project.project_name() {
debug!(
"Discovered project `{project_name}` at: {}",
project.workspace().install_path().display()
);
} else {
debug!(
"Discovered virtual workspace at: {}",
project.workspace().install_path().display()
);
}
// Determine the groups and extras to include.
let default_groups = default_dependency_groups(project.pyproject_toml())?;
let default_extras = DefaultExtras::default();
let groups = groups.with_defaults(default_groups);
let extras = extras.with_defaults(default_extras);
let venv = if isolated {
debug!("Creating isolated virtual environment");
// If we're isolating the environment, use an ephemeral virtual environment as the
// base environment for the project.
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
// Resolve the Python request and requirement for the workspace.
let WorkspacePython {
source,
python_request,
requires_python,
} = WorkspacePython::from_request(
python.as_deref().map(PythonRequest::parse),
Some(project.workspace()),
&groups,
project_dir,
no_config,
)
.await?;
let interpreter = PythonInstallation::find_or_download(
python_request.as_ref(),
EnvironmentPreference::Any,
python_preference,
python_downloads,
&client_builder,
cache,
Some(&download_reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
preview,
)
.await?
.into_interpreter();
if let Some(requires_python) = requires_python.as_ref() {
validate_project_requires_python(
&interpreter,
Some(project.workspace()),
&groups,
requires_python,
&source,
)?;
}
// Create a virtual environment
temp_dir = cache.venv_dir()?;
uv_virtualenv::create_venv(
temp_dir.path(),
interpreter,
uv_virtualenv::Prompt::None,
false,
false,
false,
false,
false,
preview,
)?
} else {
// If we're not isolating the environment, reuse the base environment for the
// project.
ProjectEnvironment::get_or_init(
project.workspace(),
&groups,
python.as_deref().map(PythonRequest::parse),
&install_mirrors,
&network_settings,
python_preference,
python_downloads,
no_sync,
no_config,
active,
cache,
DryRun::Disabled,
printer,
preview,
)
.await?
.into_environment()?
};
if no_sync {
debug!("Skipping environment synchronization due to `--no-sync`");
// If we're not syncing, we should still attempt to respect the locked preferences
// in any `--with` requirements.
if !isolated && !requirements.is_empty() {
base_lock = LockTarget::from(project.workspace())
.read()
.await
.ok()
.flatten()
.map(|lock| (lock, project.workspace().install_path().to_owned()));
}
} else {
let _lock = venv
.lock()
.await
.inspect_err(|err| {
warn!("Failed to acquire environment lock: {err}");
})
.ok();
// Determine the lock mode.
let mode = if frozen {
LockMode::Frozen
} else if locked {
LockMode::Locked(venv.interpreter())
} else {
LockMode::Write(venv.interpreter())
};
let result = match project::lock::LockOperation::new(
mode,
&settings.resolver,
&network_settings,
&lock_state,
if show_resolution {
Box::new(DefaultResolveLogger)
} else {
Box::new(SummaryResolveLogger)
},
concurrency,
cache,
&workspace_cache,
printer,
preview,
)
.execute(project.workspace().into())
.await
{
Ok(result) => result,
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
network_settings.native_tls,
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
// Identify the installation target.
let target = match &project {
VirtualProject::Project(project) => {
if all_packages {
InstallTarget::Workspace {
workspace: project.workspace(),
lock: result.lock(),
}
} else if let Some(package) = package.as_ref() {
InstallTarget::Project {
workspace: project.workspace(),
name: package,
lock: result.lock(),
}
} else {
// By default, install the root package.
InstallTarget::Project {
workspace: project.workspace(),
name: project.project_name(),
lock: result.lock(),
}
}
}
VirtualProject::NonProject(workspace) => {
if all_packages {
InstallTarget::NonProjectWorkspace {
workspace,
lock: result.lock(),
}
} else if let Some(package) = package.as_ref() {
InstallTarget::Project {
workspace,
name: package,
lock: result.lock(),
}
} else {
// By default, install the entire workspace.
InstallTarget::NonProjectWorkspace {
workspace,
lock: result.lock(),
}
}
}
};
let install_options = InstallOptions::default();
// Validate that the set of requested extras and development groups are defined in the lockfile.
target.validate_extras(&extras)?;
target.validate_groups(&groups)?;
match project::sync::do_sync(
target,
&venv,
&extras,
&groups,
editable,
install_options,
modifications,
None,
(&settings).into(),
&network_settings,
&sync_state,
if show_resolution {
Box::new(DefaultInstallLogger)
} else {
Box::new(SummaryInstallLogger)
},
installer_metadata,
concurrency,
cache,
workspace_cache.clone(),
DryRun::Disabled,
printer,
preview,
)
.await
{
Ok(()) => {}
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
network_settings.native_tls,
)
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
}
base_lock = Some((
result.into_lock(),
project.workspace().install_path().to_owned(),
));
}
venv.into_interpreter()
} else {
debug!("No project found; searching for Python interpreter");
let interpreter = {
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
// (1) Explicit request from user
let python_request = if let Some(request) = python.as_deref() {
Some(PythonRequest::parse(request))
// (2) Request from `.python-version`
} else {
PythonVersionFile::discover(
&project_dir,
&VersionFileDiscoveryOptions::default().with_no_config(no_config),
)
.await?
.and_then(PythonVersionFile::into_version)
};
let python = PythonInstallation::find_or_download(
python_request.as_ref(),
// No opt-in is required for system environments, since we are not mutating it.
EnvironmentPreference::Any,
python_preference,
python_downloads,
&client_builder,
cache,
Some(&download_reporter),
install_mirrors.python_install_mirror.as_deref(),
install_mirrors.pypy_install_mirror.as_deref(),
install_mirrors.python_downloads_json_url.as_deref(),
preview,
)
.await?;
python.into_interpreter()
};
if isolated {
debug!("Creating isolated virtual environment");
// If we're isolating the environment, use an ephemeral virtual environment.
temp_dir = cache.venv_dir()?;
let venv = uv_virtualenv::create_venv(
temp_dir.path(),
interpreter,
uv_virtualenv::Prompt::None,
false,
false,
false,
false,
false,
preview,
)?;
venv.into_interpreter()
} else {
interpreter
}
}
};
debug!(
"Using Python {} interpreter at: {}",
base_interpreter.python_version(),
base_interpreter.sys_executable().display()
);
// Read the requirements.
let spec = if requirements.is_empty() {
None
} else {
let client_builder = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone());
let spec =
RequirementsSpecification::from_simple_sources(&requirements, &client_builder).await?;
Some(spec)
};
// If necessary, create an environment for the ephemeral requirements or command.
let base_site_packages = SitePackages::from_interpreter(&base_interpreter)?;
let ephemeral_env = match spec {
None => None,
Some(spec)
if can_skip_ephemeral(&spec, &base_interpreter, &base_site_packages, &settings) =>
{
None
}
Some(spec) => {
debug!("Syncing ephemeral requirements");
// Read the build constraints from the lock file.
let build_constraints = base_lock
.as_ref()
.map(|(lock, path)| lock.build_constraints(path));
// Read the preferences.
let spec = EnvironmentSpecification::from(spec).with_preferences(
if let Some((lock, install_path)) = base_lock.as_ref() {
// If we have a lockfile, use the locked versions as preferences.
PreferenceLocation::Lock { lock, install_path }
} else {
// Otherwise, extract preferences from the base environment.
PreferenceLocation::Entries(
base_site_packages
.iter()
.filter_map(Preference::from_installed)
.collect::<Vec<_>>(),
)
},
);
let result = CachedEnvironment::from_spec(
spec,
build_constraints.unwrap_or_default(),
&base_interpreter,
&settings,
&network_settings,
&sync_state,
if show_resolution {
Box::new(DefaultResolveLogger)
} else {
Box::new(SummaryResolveLogger)
},
if show_resolution {
Box::new(DefaultInstallLogger)
} else {
Box::new(SummaryInstallLogger)
},
installer_metadata,
concurrency,
cache,
printer,
preview,
)
.await;
let environment = match result {
Ok(resolution) => resolution,
Err(ProjectError::Operation(err)) => {
return diagnostics::OperationDiagnostic::native_tls(
network_settings.native_tls,
)
.with_context("`--with`")
.report(err)
.map_or(Ok(ExitStatus::Failure), |err| Err(err.into()));
}
Err(err) => return Err(err.into()),
};
Some(environment)
}
};
// If we're running in an ephemeral environment, add a path file to enable loading of
// the base environment's site packages. Setting `PYTHONPATH` is insufficient, as it doesn't
// resolve `.pth` files in the base environment.
//
// `sitecustomize.py` would be an alternative, but it can be shadowed by an existing such
// module in the python installation.
if let Some(ephemeral_env) = ephemeral_env.as_ref() {
let site_packages = base_interpreter
.site_packages()
.next()
.ok_or_else(|| ProjectError::NoSitePackages)?;
ephemeral_env.set_overlay(format!(
"import site; site.addsitedir(\"{}\")",
site_packages.escape_for_python()
))?;
// Write the `sys.prefix` of the parent environment to the `extends-environment` key of the `pyvenv.cfg`
// file. This helps out static-analysis tools such as ty (see docs on
// `CachedEnvironment::set_parent_environment`).
//
// Note that we do this even if the parent environment is not a virtual environment.
// For ephemeral environments created by `uv run --with`, the parent environment's
// `site-packages` directory is added to `sys.path` even if the parent environment is not
// a virtual environment and even if `--system-site-packages` was not explicitly selected.
ephemeral_env.set_parent_environment(base_interpreter.sys_prefix())?;
// If `--system-site-packages` is enabled, add the system site packages to the ephemeral
// environment.
if base_interpreter.is_virtualenv()
&& PyVenvConfiguration::parse(base_interpreter.sys_prefix().join("pyvenv.cfg"))
.is_ok_and(|cfg| cfg.include_system_site_packages())
{
ephemeral_env.set_system_site_packages()?;
} else {
ephemeral_env.clear_system_site_packages()?;
}
}
// Cast from `CachedEnvironment` to `PythonEnvironment`.
let ephemeral_env = ephemeral_env.map(PythonEnvironment::from);
// Determine the Python interpreter to use for the command, if necessary.
let interpreter = ephemeral_env
.as_ref()
.map_or_else(|| &base_interpreter, |env| env.interpreter());
// Check if any run command is given.
// If not, print the available scripts for the current interpreter.
let Some(command) = command else {
writeln!(
printer.stdout(),
"Provide a command or script to invoke with `uv run <command>` or `uv run <script>.py`.\n"
)?;
#[allow(clippy::map_identity)]
let commands = interpreter
.scripts()
.read_dir()
.ok()
.into_iter()
.flatten()
.map(|entry| match entry {
Ok(entry) => Ok(entry),
Err(err) => {
// If we can't read the entry, fail.
// This could be a symptom of a more serious problem.
warn!("Failed to read entry: {}", err);
Err(err)
}
})
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.filter(|entry| {
entry
.file_type()
.is_ok_and(|file_type| file_type.is_file() || file_type.is_symlink())
})
.map(|entry| entry.path())
.filter(|path| is_executable(path))
.map(|path| {
if cfg!(windows)
&& path
.extension()
.is_some_and(|exe| exe == std::env::consts::EXE_EXTENSION)
{
// Remove the extensions.
path.with_extension("")
} else {
path
}
})
.map(|path| {
path.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string()
})
.filter(|command| {
!command.starts_with("activate") && !command.starts_with("deactivate")
})
.sorted()
.collect_vec();
if !commands.is_empty() {
writeln!(
printer.stdout(),
"The following commands are available in the environment:\n"
)?;
for command in commands {
writeln!(printer.stdout(), "- {command}")?;
}
}
let help = format!("See `{}` for more information.", "uv run --help".bold());
writeln!(printer.stdout(), "\n{help}")?;
return Ok(ExitStatus::Error);
};
debug!("Running `{command}`");
let mut process = command.as_command(interpreter);
// Construct the `PATH` environment variable.
let new_path = std::env::join_paths(
ephemeral_env
.as_ref()
.map(PythonEnvironment::scripts)
.into_iter()
.chain(std::iter::once(base_interpreter.scripts()))
.chain(
// On Windows, non-virtual Python distributions put `python.exe` in the top-level
// directory, rather than in the `Scripts` subdirectory.
cfg!(windows)
.then(|| base_interpreter.sys_executable().parent())
.flatten()
.into_iter(),
)
.dedup()
.map(PathBuf::from)
.chain(
std::env::var_os(EnvVars::PATH)
.as_ref()
.iter()
.flat_map(std::env::split_paths),
),
)?;
process.env(EnvVars::PATH, new_path);
// Increment recursion depth counter.
process.env(
EnvVars::UV_RUN_RECURSION_DEPTH,
(recursion_depth + 1).to_string(),
);
// Ensure `VIRTUAL_ENV` is set.
if interpreter.is_virtualenv() {
process.env(EnvVars::VIRTUAL_ENV, interpreter.sys_prefix().as_os_str());
}
// Spawn and wait for completion
// Standard input, output, and error streams are all inherited
// TODO(zanieb): Throw a nicer error message if the command is not found
let handle = process
.spawn()
.with_context(|| format!("Failed to spawn: `{}`", command.display_executable()))?;
run_to_completion(handle).await
}
/// Returns `true` if we can skip creating an additional ephemeral environment in `uv run`.
fn can_skip_ephemeral(
spec: &RequirementsSpecification,
interpreter: &Interpreter,
site_packages: &SitePackages,
settings: &ResolverInstallerSettings,
) -> bool {
if !(settings.reinstall.is_none() && settings.reinstall.is_none()) {
return false;
}
match site_packages.satisfies_spec(
&spec.requirements,
&spec.constraints,
&spec.overrides,
&interpreter.resolver_marker_environment(),
) {
// If the requirements are already satisfied, we're done.
Ok(SatisfiesResult::Fresh {
recursive_requirements,
}) => {
debug!(
"Base environment satisfies requirements: {}",
recursive_requirements
.iter()
.map(ToString::to_string)
.sorted()
.join(" | ")
);
true
}
Ok(SatisfiesResult::Unsatisfied(requirement)) => {
debug!(
"At least one requirement is not satisfied in the base environment: {requirement}"
);
false
}
Err(err) => {
debug!("Failed to check requirements against base environment: {err}");
false
}
}
}
#[derive(Debug)]
pub(crate) enum RunCommand {
/// Execute `python`.
Python(Vec<OsString>),
/// Execute a `python` script.
PythonScript(PathBuf, Vec<OsString>),
/// Search `sys.path` for the named module and execute its contents as the `__main__` module.
/// Equivalent to `python -m module`.
PythonModule(OsString, Vec<OsString>),
/// Execute a `pythonw` GUI script.
PythonGuiScript(PathBuf, Vec<OsString>),
/// Execute a Python package containing a `__main__.py` file.
/// If an entrypoint with the target name is installed in the environment, it is preferred.
PythonPackage(OsString, PathBuf, Vec<OsString>),
/// Execute a Python [zipapp].
/// [zipapp]: <https://docs.python.org/3/library/zipapp.html>
PythonZipapp(PathBuf, Vec<OsString>),
/// Execute a `python` script provided via `stdin`.
PythonStdin(Vec<u8>, Vec<OsString>),
/// Execute a `pythonw` script provided via `stdin`.
PythonGuiStdin(Vec<u8>, Vec<OsString>),
/// Execute a Python script provided via a remote URL.
PythonRemote(DisplaySafeUrl, tempfile::NamedTempFile, Vec<OsString>),
/// Execute an external command.
External(OsString, Vec<OsString>),
/// Execute an empty command (in practice, `python` with no arguments).
Empty,
}
impl RunCommand {
/// Return the name of the target executable, for display purposes.
fn display_executable(&self) -> Cow<'_, str> {
match self {
Self::Python(_)
| Self::PythonScript(..)
| Self::PythonZipapp(..)
| Self::PythonRemote(..)
| Self::Empty => Cow::Borrowed("python"),
// N.B. We can't know if we'll invoke `<target>` or `python <target>` without checking
// the available scripts in the interpreter — we could improve this message
Self::PythonPackage(target, ..) => target.to_string_lossy(),
Self::PythonModule(..) => Cow::Borrowed("python -m"),
Self::PythonGuiScript(..) => {
if cfg!(windows) {
Cow::Borrowed("pythonw")
} else {
Cow::Borrowed("python")
}
}
Self::PythonStdin(..) => Cow::Borrowed("python -c"),
Self::PythonGuiStdin(..) => {
if cfg!(windows) {
Cow::Borrowed("pythonw -c")
} else {
Cow::Borrowed("python -c")
}
}
Self::External(executable, _) => executable.to_string_lossy(),
}
}
/// Convert a [`RunCommand`] into a [`Command`].
fn as_command(&self, interpreter: &Interpreter) -> Command {
match self {
Self::Python(args) => {
let mut process = Command::new(interpreter.sys_executable());
process.args(args);
process
}
Self::PythonPackage(target, path, args) => {
let name = PathBuf::from(target).with_extension(std::env::consts::EXE_EXTENSION);
let entrypoint = interpreter.scripts().join(name);
// If the target is an installed, executable script — prefer that
if uv_fs::which::is_executable(&entrypoint) {
let mut process = Command::new(entrypoint);
process.args(args);
process
// Otherwise, invoke `python <module>`
} else {
let mut process = Command::new(interpreter.sys_executable());
process.arg(path);
process.args(args);
process
}
}
Self::PythonScript(target, args) | Self::PythonZipapp(target, args) => {
let mut process = Command::new(interpreter.sys_executable());
process.arg(target);
process.args(args);
process
}
Self::PythonRemote(.., target, args) => {
let mut process = Command::new(interpreter.sys_executable());
process.arg(target.path());
process.args(args);
process
}
Self::PythonModule(module, args) => {
let mut process = Command::new(interpreter.sys_executable());
process.arg("-m");
process.arg(module);
process.args(args);
process
}
Self::PythonGuiScript(target, args) => {
let python_executable = interpreter.sys_executable();
// Use `pythonw.exe` if it exists, otherwise fall back to `python.exe`.
// See `install-wheel-rs::get_script_executable`.gd
let pythonw_executable = python_executable
.file_name()
.map(|name| {
let new_name = name.to_string_lossy().replace("python", "pythonw");
python_executable.with_file_name(new_name)
})
.filter(|path| path.is_file())
.unwrap_or_else(|| python_executable.to_path_buf());
let mut process = Command::new(&pythonw_executable);
process.arg(target);
process.args(args);
process
}
Self::PythonStdin(script, args) => {
let mut process = Command::new(interpreter.sys_executable());
process.arg("-c");
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
process.arg(OsString::from_vec(script.clone()));
}
#[cfg(not(unix))]
{
let script = String::from_utf8(script.clone()).expect("script is valid UTF-8");
process.arg(script);
}
process.args(args);
process
}
Self::PythonGuiStdin(script, args) => {
let python_executable = interpreter.sys_executable();
// Use `pythonw.exe` if it exists, otherwise fall back to `python.exe`.
// See `install-wheel-rs::get_script_executable`.gd
let pythonw_executable = python_executable
.file_name()
.map(|name| {
let new_name = name.to_string_lossy().replace("python", "pythonw");
python_executable.with_file_name(new_name)
})
.filter(|path| path.is_file())
.unwrap_or_else(|| python_executable.to_path_buf());
let mut process = Command::new(&pythonw_executable);
process.arg("-c");
#[cfg(unix)]
{
use std::os::unix::ffi::OsStringExt;
process.arg(OsString::from_vec(script.clone()));
}
#[cfg(not(unix))]
{
let script = String::from_utf8(script.clone()).expect("script is valid UTF-8");
process.arg(script);
}
process.args(args);
process
}
Self::External(executable, args) => {
let mut process = if cfg!(windows) {
WindowsRunnable::from_script_path(interpreter.scripts(), executable).into()
} else {
Command::new(executable)
};
process.args(args);
process
}
Self::Empty => Command::new(interpreter.sys_executable()),
}
}
}
impl std::fmt::Display for RunCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Python(args) => {
write!(f, "python")?;
for arg in args {
write!(f, " {}", arg.to_string_lossy())?;
}
Ok(())
}
Self::PythonPackage(target, _path, args) => {
write!(f, "{}", target.to_string_lossy())?;
for arg in args {
write!(f, " {}", arg.to_string_lossy())?;
}
Ok(())
}
Self::PythonScript(target, args) | Self::PythonZipapp(target, args) => {
write!(f, "python {}", target.display())?;
for arg in args {
write!(f, " {}", arg.to_string_lossy())?;
}
Ok(())
}
Self::PythonModule(module, args) => {
write!(f, "python -m")?;
write!(f, " {}", module.to_string_lossy())?;
for arg in args {
write!(f, " {}", arg.to_string_lossy())?;
}
Ok(())
}
Self::PythonGuiScript(target, args) => {
write!(f, "pythonw {}", target.display())?;
for arg in args {
write!(f, " {}", arg.to_string_lossy())?;
}
Ok(())
}
Self::PythonStdin(..) | Self::PythonRemote(..) => {
write!(f, "python -c")?;
Ok(())
}
Self::PythonGuiStdin(..) => {
write!(f, "pythonw -c")?;
Ok(())
}
Self::External(executable, args) => {
write!(f, "{}", executable.to_string_lossy())?;
for arg in args {
write!(f, " {}", arg.to_string_lossy())?;
}
Ok(())
}
Self::Empty => {
write!(f, "python")?;
Ok(())
}
}
}
}
impl RunCommand {
/// Determine the [`RunCommand`] for a given set of arguments.
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn from_args(
command: &ExternalCommand,
network_settings: NetworkSettings,
module: bool,
script: bool,
gui_script: bool,
) -> anyhow::Result<Self> {
let (target, args) = command.split();
let Some(target) = target else {
return Ok(Self::Empty);
};
if target.eq_ignore_ascii_case("-") {
let mut buf = Vec::with_capacity(1024);
std::io::stdin().read_to_end(&mut buf)?;
return if module {
Err(anyhow!("Cannot run a Python module from stdin"))
} else if gui_script {
Ok(Self::PythonGuiStdin(buf, args.to_vec()))
} else {
Ok(Self::PythonStdin(buf, args.to_vec()))
};
}
let target_path = PathBuf::from(target);
// Determine whether the user provided a remote script.
if target_path.starts_with("http://") || target_path.starts_with("https://") {
// Only continue if we are absolutely certain no local file exists.
//
// We don't do this check on Windows since the file path would
// be invalid anyway, and thus couldn't refer to a local file.
if !cfg!(unix) || matches!(target_path.try_exists(), Ok(false)) {
let url = DisplaySafeUrl::parse(&target.to_string_lossy())?;
let file_stem = url
.path_segments()
.and_then(Iterator::last)
.and_then(|segment| segment.strip_suffix(".py"))
.unwrap_or("script");
let file = tempfile::Builder::new()
.prefix(file_stem)
.suffix(".py")
.tempfile()?;
let client = BaseClientBuilder::new()
.retries_from_env()?
.connectivity(network_settings.connectivity)
.native_tls(network_settings.native_tls)
.allow_insecure_host(network_settings.allow_insecure_host.clone())
.build();
let response = client
.for_host(&url)
.get(Url::from(url.clone()))
.send()
.await?;
// Stream the response to the file.
let mut writer = file.as_file();
let mut reader = response.bytes_stream();
while let Some(chunk) = reader.next().await {
use std::io::Write;
writer.write_all(&chunk?)?;
}
return Ok(Self::PythonRemote(url, file, args.to_vec()));
}
}
if module {
return Ok(Self::PythonModule(target.clone(), args.to_vec()));
} else if gui_script {
return Ok(Self::PythonGuiScript(target.clone().into(), args.to_vec()));
} else if script {
return Ok(Self::PythonScript(target.clone().into(), args.to_vec()));
}
let metadata = target_path.metadata();
let is_file = metadata.as_ref().is_ok_and(std::fs::Metadata::is_file);
let is_dir = metadata.as_ref().is_ok_and(std::fs::Metadata::is_dir);
if target.eq_ignore_ascii_case("python") {
Ok(Self::Python(args.to_vec()))
} else if target_path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("py") || ext.eq_ignore_ascii_case("pyc"))
&& is_file
{
Ok(Self::PythonScript(target_path, args.to_vec()))
} else if target_path
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("pyw"))
&& is_file
{
Ok(Self::PythonGuiScript(target_path, args.to_vec()))
} else if is_dir && target_path.join("__main__.py").is_file() {
Ok(Self::PythonPackage(
target.clone(),
target_path,
args.to_vec(),
))
} else if is_file && is_python_zipapp(&target_path) {
Ok(Self::PythonZipapp(target_path, args.to_vec()))
} else {
Ok(Self::External(
target.clone(),
args.iter().map(std::clone::Clone::clone).collect(),
))
}
}
}
/// Returns `true` if the target is a ZIP archive containing a `__main__.py` file.
fn is_python_zipapp(target: &Path) -> bool {
if let Ok(file) = fs_err::File::open(target) {
if let Ok(mut archive) = zip::ZipArchive::new(file) {
return archive.by_name("__main__.py").is_ok_and(|f| f.is_file());
}
}
false
}
/// Read and parse recursion depth from the environment.
///
/// Returns Ok(0) if `EnvVars::UV_RUN_RECURSION_DEPTH` is not set.
///
/// Returns an error if `EnvVars::UV_RUN_RECURSION_DEPTH` is set to a value
/// that cannot ber parsed as an integer.
fn read_recursion_depth_from_environment_variable() -> anyhow::Result<u32> {
let envvar = match std::env::var(EnvVars::UV_RUN_RECURSION_DEPTH) {
Ok(val) => val,
Err(VarError::NotPresent) => return Ok(0),
Err(e) => {
return Err(e)
.with_context(|| format!("invalid value for {}", EnvVars::UV_RUN_RECURSION_DEPTH));
}
};
envvar
.parse::<u32>()
.with_context(|| format!("invalid value for {}", EnvVars::UV_RUN_RECURSION_DEPTH))
}