uv/crates/uv/src/commands/project/run.rs
konsti 081f20c53e
Add support for tool.uv into distribution building (#3904)
With the change, we remove the special casing of workspace dependencies
and resolve `tool.uv` for all git and directory distributions. This
gives us support for non-editable workspace dependencies and path
dependencies in other workspaces. It removes a lot of special casing
around workspaces. These changes are the groundwork for supporting
`tool.uv` with dynamic metadata.

The basis for this change is moving `Requirement` from
`distribution-types` to `pypi-types` and the lowering logic from
`uv-requirements` to `uv-distribution`. This changes should be split out
in separate PRs.

I've included an example workspace `albatross-root-workspace2` where
`bird-feeder` depends on `a` from another workspace `ab`. There's a
bunch of failing tests and regressed error messages that still need
fixing. It does fix the audited package count for the workspace tests.
2024-05-31 02:42:03 +00:00

195 lines
6.1 KiB
Rust

use std::ffi::OsString;
use std::path::PathBuf;
use anyhow::{Context, Result};
use itertools::Itertools;
use tempfile::tempdir_in;
use tokio::process::Command;
use tracing::debug;
use uv_cache::Cache;
use uv_client::Connectivity;
use uv_configuration::{ExtrasSpecification, PreviewMode, Upgrade};
use uv_distribution::ProjectWorkspace;
use uv_interpreter::{PythonEnvironment, SystemPython};
use uv_requirements::RequirementsSource;
use uv_resolver::ExcludeNewer;
use uv_warnings::warn_user;
use crate::commands::{project, ExitStatus};
use crate::printer::Printer;
/// Run a command.
#[allow(clippy::too_many_arguments)]
pub(crate) async fn run(
extras: ExtrasSpecification,
target: Option<String>,
mut args: Vec<OsString>,
requirements: Vec<RequirementsSource>,
python: Option<String>,
upgrade: Upgrade,
exclude_newer: Option<ExcludeNewer>,
isolated: bool,
preview: PreviewMode,
connectivity: Connectivity,
cache: &Cache,
printer: Printer,
) -> Result<ExitStatus> {
if preview.is_disabled() {
warn_user!("`uv run` is experimental and may change without warning.");
}
// Discover and sync the project.
let project_env = if isolated {
None
} else {
debug!("Syncing project environment.");
let project = ProjectWorkspace::discover(std::env::current_dir()?).await?;
let venv = project::init_environment(&project, preview, cache, printer)?;
// Lock and sync the environment.
let lock = project::lock::do_lock(
&project,
&venv,
upgrade,
exclude_newer,
preview,
cache,
printer,
)
.await?;
project::sync::do_sync(&project, &venv, &lock, extras, preview, cache, printer).await?;
Some(venv)
};
// If necessary, create an environment for the ephemeral requirements.
let tmpdir;
let ephemeral_env = if requirements.is_empty() {
None
} else {
debug!("Syncing ephemeral environment.");
// Discover an interpreter.
let interpreter = if let Some(project_env) = &project_env {
project_env.interpreter().clone()
} else if let Some(python) = python.as_ref() {
PythonEnvironment::from_requested_python(python, SystemPython::Allowed, preview, cache)?
.into_interpreter()
} else {
PythonEnvironment::from_default_python(preview, cache)?.into_interpreter()
};
// TODO(charlie): If the environment satisfies the requirements, skip creation.
// TODO(charlie): Pass the already-installed versions as preferences, or even as the
// "installed" packages, so that we can skip re-installing them in the ephemeral
// environment.
// Create a virtual environment
// TODO(zanieb): Move this path derivation elsewhere
let uv_state_path = std::env::current_dir()?.join(".uv");
fs_err::create_dir_all(&uv_state_path)?;
tmpdir = tempdir_in(uv_state_path)?;
let venv = uv_virtualenv::create_venv(
tmpdir.path(),
interpreter,
uv_virtualenv::Prompt::None,
false,
false,
)?;
// Install the ephemeral requirements.
Some(
project::update_environment(venv, &requirements, connectivity, cache, printer, preview)
.await?,
)
};
// Construct the command
let command = if let Some(target) = target {
let target_path = PathBuf::from(&target);
if target_path
.extension()
.map_or(false, |ext| ext.eq_ignore_ascii_case("py"))
&& target_path.exists()
{
args.insert(0, target_path.as_os_str().into());
"python".to_string()
} else {
target
}
} else {
"python".to_string()
};
let mut process = Command::new(&command);
process.args(&args);
// Construct the `PATH` environment variable.
let new_path = std::env::join_paths(
ephemeral_env
.as_ref()
.map(PythonEnvironment::scripts)
.into_iter()
.chain(
project_env
.as_ref()
.map(PythonEnvironment::scripts)
.into_iter(),
)
.map(PathBuf::from)
.chain(
std::env::var_os("PATH")
.as_ref()
.iter()
.flat_map(std::env::split_paths),
),
)?;
process.env("PATH", new_path);
// Construct the `PYTHONPATH` environment variable.
let new_python_path = std::env::join_paths(
ephemeral_env
.as_ref()
.map(PythonEnvironment::site_packages)
.into_iter()
.flatten()
.chain(
project_env
.as_ref()
.map(PythonEnvironment::site_packages)
.into_iter()
.flatten(),
)
.map(PathBuf::from)
.chain(
std::env::var_os("PYTHONPATH")
.as_ref()
.iter()
.flat_map(std::env::split_paths),
),
)?;
process.env("PYTHONPATH", new_python_path);
// 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 space = if args.is_empty() { "" } else { " " };
debug!(
"Running `{command}{space}{}`",
args.iter().map(|arg| arg.to_string_lossy()).join(" ")
);
let mut handle = process
.spawn()
.with_context(|| format!("Failed to spawn: `{command}`"))?;
let status = handle.wait().await.context("Child process disappeared")?;
// Exit based on the result of the command
// TODO(zanieb): Do we want to exit with the code of the child process? Probably.
if status.success() {
Ok(ExitStatus::Success)
} else {
Ok(ExitStatus::Failure)
}
}