mirror of https://github.com/astral-sh/uv
Add basic `uv sync` and `uv lock` commands (#3436)
## Summary These aren't intended for production use; instead, I'm just trying to frame out the overall data flows and code-sharing for these commands. We now have `uv sync` (sync the environment to match the lockfile, without refreshing or resolving) and `uv lock` (generate the lockfile). Both _require_ a virtual environment to exist (something we should change). `uv sync`, `uv run`, and `uv lock` all share code for the underlying subroutines (resolution and installation), so the commands themselves are relatively small (~100 lines) and mostly consist of reading arguments and such. `uv lock` and `uv sync` don't actually really work yet, because we have no way to include the project itself in the lockfile (that's a TODO in the lockfile implementation). Closes https://github.com/astral-sh/uv/issues/3432.
This commit is contained in:
parent
0228b15baf
commit
76a3ceb2ca
|
|
@ -1,8 +1,9 @@
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::PythonEnvironment;
|
||||||
|
|
||||||
/// Whether to enforce build isolation when building source distributions.
|
/// Whether to enforce build isolation when building source distributions.
|
||||||
#[derive(Debug, Copy, Clone)]
|
#[derive(Debug, Default, Copy, Clone)]
|
||||||
pub enum BuildIsolation<'a> {
|
pub enum BuildIsolation<'a> {
|
||||||
|
#[default]
|
||||||
Isolated,
|
Isolated,
|
||||||
Shared(&'a PythonEnvironment),
|
Shared(&'a PythonEnvironment),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,10 @@ use pep508_rs::MarkerEnvironment;
|
||||||
use pypi_types::{HashDigest, HashError};
|
use pypi_types::{HashDigest, HashError};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Default, Clone)]
|
||||||
pub enum HashStrategy {
|
pub enum HashStrategy {
|
||||||
/// No hash policy is specified.
|
/// No hash policy is specified.
|
||||||
|
#[default]
|
||||||
None,
|
None,
|
||||||
/// Hashes should be generated (specifically, a SHA-256 hash), but not validated.
|
/// Hashes should be generated (specifically, a SHA-256 hash), but not validated.
|
||||||
Generate,
|
Generate,
|
||||||
|
|
|
||||||
|
|
@ -133,8 +133,15 @@ pub(crate) enum Commands {
|
||||||
/// Clear the cache, removing all entries or those linked to specific packages.
|
/// Clear the cache, removing all entries or those linked to specific packages.
|
||||||
#[command(hide = true)]
|
#[command(hide = true)]
|
||||||
Clean(CleanArgs),
|
Clean(CleanArgs),
|
||||||
|
/// Run a command in the project environment.
|
||||||
#[clap(hide = true)]
|
#[clap(hide = true)]
|
||||||
Run(RunArgs),
|
Run(RunArgs),
|
||||||
|
/// Sync the project's dependencies with the environment.
|
||||||
|
#[clap(hide = true)]
|
||||||
|
Sync(SyncArgs),
|
||||||
|
/// Resolve the project requirements into a lockfile.
|
||||||
|
#[clap(hide = true)]
|
||||||
|
Lock(LockArgs),
|
||||||
/// Display uv's version
|
/// Display uv's version
|
||||||
Version {
|
Version {
|
||||||
#[arg(long, value_enum, default_value = "text")]
|
#[arg(long, value_enum, default_value = "text")]
|
||||||
|
|
@ -1852,6 +1859,54 @@ pub(crate) struct RunArgs {
|
||||||
pub(crate) python: Option<String>,
|
pub(crate) python: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
pub(crate) struct SyncArgs {
|
||||||
|
/// The Python interpreter to use to build the run environment.
|
||||||
|
///
|
||||||
|
/// By default, `uv` uses the virtual environment in the current working directory or any parent
|
||||||
|
/// directory, falling back to searching for a Python executable in `PATH`. The `--python`
|
||||||
|
/// option allows you to specify a different interpreter.
|
||||||
|
///
|
||||||
|
/// Supported formats:
|
||||||
|
/// - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
|
||||||
|
/// `python3.10` on Linux and macOS.
|
||||||
|
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
|
||||||
|
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short,
|
||||||
|
env = "UV_PYTHON",
|
||||||
|
verbatim_doc_comment,
|
||||||
|
group = "discovery"
|
||||||
|
)]
|
||||||
|
pub(crate) python: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
pub(crate) struct LockArgs {
|
||||||
|
/// The Python interpreter to use to build the run environment.
|
||||||
|
///
|
||||||
|
/// By default, `uv` uses the virtual environment in the current working directory or any parent
|
||||||
|
/// directory, falling back to searching for a Python executable in `PATH`. The `--python`
|
||||||
|
/// option allows you to specify a different interpreter.
|
||||||
|
///
|
||||||
|
/// Supported formats:
|
||||||
|
/// - `3.10` looks for an installed Python 3.10 using `py --list-paths` on Windows, or
|
||||||
|
/// `python3.10` on Linux and macOS.
|
||||||
|
/// - `python3.10` or `python.exe` looks for a binary with the given name in `PATH`.
|
||||||
|
/// - `/home/ferris/.local/bin/python3.10` uses the exact Python at the given path.
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
short,
|
||||||
|
env = "UV_PYTHON",
|
||||||
|
verbatim_doc_comment,
|
||||||
|
group = "discovery"
|
||||||
|
)]
|
||||||
|
pub(crate) python: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
struct AddArgs {
|
struct AddArgs {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ pub(crate) use pip_list::pip_list;
|
||||||
pub(crate) use pip_show::pip_show;
|
pub(crate) use pip_show::pip_show;
|
||||||
pub(crate) use pip_sync::pip_sync;
|
pub(crate) use pip_sync::pip_sync;
|
||||||
pub(crate) use pip_uninstall::pip_uninstall;
|
pub(crate) use pip_uninstall::pip_uninstall;
|
||||||
pub(crate) use run::run;
|
|
||||||
#[cfg(feature = "self-update")]
|
#[cfg(feature = "self-update")]
|
||||||
pub(crate) use self_update::self_update;
|
pub(crate) use self_update::self_update;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
|
|
@ -26,6 +25,9 @@ use uv_interpreter::PythonEnvironment;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
pub(crate) use venv::venv;
|
pub(crate) use venv::venv;
|
||||||
pub(crate) use version::version;
|
pub(crate) use version::version;
|
||||||
|
pub(crate) use workspace::lock::lock;
|
||||||
|
pub(crate) use workspace::run::run;
|
||||||
|
pub(crate) use workspace::sync::sync;
|
||||||
|
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
|
|
||||||
|
|
@ -41,11 +43,11 @@ mod pip_show;
|
||||||
mod pip_sync;
|
mod pip_sync;
|
||||||
mod pip_uninstall;
|
mod pip_uninstall;
|
||||||
mod reporters;
|
mod reporters;
|
||||||
mod run;
|
|
||||||
#[cfg(feature = "self-update")]
|
#[cfg(feature = "self-update")]
|
||||||
mod self_update;
|
mod self_update;
|
||||||
mod venv;
|
mod venv;
|
||||||
mod version;
|
mod version;
|
||||||
|
mod workspace;
|
||||||
|
|
||||||
#[derive(Copy, Clone)]
|
#[derive(Copy, Clone)]
|
||||||
pub(crate) enum ExitStatus {
|
pub(crate) enum ExitStatus {
|
||||||
|
|
|
||||||
|
|
@ -1,736 +0,0 @@
|
||||||
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
|
||||||
use crate::commands::ExitStatus;
|
|
||||||
use crate::commands::{elapsed, ChangeEvent, ChangeEventKind};
|
|
||||||
use crate::printer::Printer;
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use distribution_types::{
|
|
||||||
IndexLocations, InstalledMetadata, LocalDist, Name, Requirement, Resolution,
|
|
||||||
};
|
|
||||||
use install_wheel_rs::linker::LinkMode;
|
|
||||||
use itertools::Itertools;
|
|
||||||
use owo_colors::OwoColorize;
|
|
||||||
use pep508_rs::{MarkerEnvironment, PackageName};
|
|
||||||
use platform_tags::Tags;
|
|
||||||
use pypi_types::Yanked;
|
|
||||||
use std::ffi::OsString;
|
|
||||||
use std::fmt::Write;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::{env, iter};
|
|
||||||
use tempfile::{tempdir_in, TempDir};
|
|
||||||
use tokio::process::Command;
|
|
||||||
use tracing::debug;
|
|
||||||
use uv_warnings::warn_user;
|
|
||||||
|
|
||||||
use uv_cache::Cache;
|
|
||||||
use uv_client::{BaseClientBuilder, RegistryClient, RegistryClientBuilder};
|
|
||||||
use uv_configuration::{
|
|
||||||
ConfigSettings, Constraints, NoBinary, NoBuild, Overrides, PreviewMode, Reinstall,
|
|
||||||
SetupPyStrategy,
|
|
||||||
};
|
|
||||||
use uv_dispatch::BuildDispatch;
|
|
||||||
use uv_fs::Simplified;
|
|
||||||
use uv_installer::{Downloader, Plan, Planner, SatisfiesResult, SitePackages};
|
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
|
||||||
use uv_requirements::{
|
|
||||||
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
|
|
||||||
RequirementsSpecification, SourceTreeResolver,
|
|
||||||
};
|
|
||||||
use uv_resolver::{
|
|
||||||
Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, OptionsBuilder, ResolutionGraph,
|
|
||||||
Resolver,
|
|
||||||
};
|
|
||||||
use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight};
|
|
||||||
|
|
||||||
/// Run a command.
|
|
||||||
#[allow(clippy::unnecessary_wraps, clippy::too_many_arguments)]
|
|
||||||
pub(crate) async fn run(
|
|
||||||
target: Option<String>,
|
|
||||||
mut args: Vec<OsString>,
|
|
||||||
mut requirements: Vec<RequirementsSource>,
|
|
||||||
python: Option<String>,
|
|
||||||
isolated: bool,
|
|
||||||
preview: PreviewMode,
|
|
||||||
cache: &Cache,
|
|
||||||
printer: Printer,
|
|
||||||
) -> Result<ExitStatus> {
|
|
||||||
if preview.is_disabled() {
|
|
||||||
warn_user!("`uv run` is experimental and may change without warning.");
|
|
||||||
}
|
|
||||||
|
|
||||||
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()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Copy the requirements into a set of overrides; we'll use this to prioritize
|
|
||||||
// requested requirements over those discovered in the project.
|
|
||||||
// We must retain these requirements as direct dependencies too, as overrides
|
|
||||||
// cannot be applied to transitive dependencies.
|
|
||||||
let overrides = requirements.clone();
|
|
||||||
|
|
||||||
if !isolated {
|
|
||||||
if let Some(workspace_requirements) = find_workspace_requirements()? {
|
|
||||||
requirements.extend(workspace_requirements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect the current Python interpreter.
|
|
||||||
// TODO(zanieb): Create ephemeral environments
|
|
||||||
// TODO(zanieb): Accept `--python`
|
|
||||||
let run_env = environment_for_run(
|
|
||||||
&requirements,
|
|
||||||
&overrides,
|
|
||||||
python.as_deref(),
|
|
||||||
isolated,
|
|
||||||
preview,
|
|
||||||
cache,
|
|
||||||
printer,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
let python_env = run_env.python;
|
|
||||||
|
|
||||||
// Construct the command
|
|
||||||
let mut process = Command::new(&command);
|
|
||||||
process.args(&args);
|
|
||||||
|
|
||||||
// Set up the PATH
|
|
||||||
debug!(
|
|
||||||
"Using Python {} environment at {}",
|
|
||||||
python_env.interpreter().python_version(),
|
|
||||||
python_env.python_executable().user_display().cyan()
|
|
||||||
);
|
|
||||||
let new_path = if let Some(path) = std::env::var_os("PATH") {
|
|
||||||
let python_env_path =
|
|
||||||
iter::once(python_env.scripts().to_path_buf()).chain(env::split_paths(&path));
|
|
||||||
env::join_paths(python_env_path)?
|
|
||||||
} else {
|
|
||||||
OsString::from(python_env.scripts())
|
|
||||||
};
|
|
||||||
|
|
||||||
process.env("PATH", new_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()?;
|
|
||||||
let status = handle.wait().await?;
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct RunEnvironment {
|
|
||||||
/// The Python environment to execute the run in.
|
|
||||||
python: PythonEnvironment,
|
|
||||||
/// A temporary directory, if a new virtual environment was created.
|
|
||||||
///
|
|
||||||
/// Included to ensure that the temporary directory exists for the length of the operation, but
|
|
||||||
/// is dropped at the end as appropriate.
|
|
||||||
_temp_dir_drop: Option<TempDir>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn find_workspace_requirements() -> Result<Option<Vec<RequirementsSource>>> {
|
|
||||||
// TODO(zanieb): Add/use workspace logic to load requirements for a workspace
|
|
||||||
// We cannot use `Workspace::find` yet because it depends on a `[tool.uv]` section
|
|
||||||
let pyproject_path = std::env::current_dir()?.join("pyproject.toml");
|
|
||||||
if pyproject_path.exists() {
|
|
||||||
debug!(
|
|
||||||
"Loading requirements from {}",
|
|
||||||
pyproject_path.user_display()
|
|
||||||
);
|
|
||||||
return Ok(Some(vec![
|
|
||||||
RequirementsSource::from_requirements_file(pyproject_path),
|
|
||||||
RequirementsSource::from_package(".".to_string()),
|
|
||||||
]));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns an environment for a `run` invocation.
|
|
||||||
///
|
|
||||||
/// Will use the current virtual environment (if any) unless `isolated` is true.
|
|
||||||
/// Will create virtual environments in a temporary directory (if necessary).
|
|
||||||
async fn environment_for_run(
|
|
||||||
requirements: &[RequirementsSource],
|
|
||||||
overrides: &[RequirementsSource],
|
|
||||||
python: Option<&str>,
|
|
||||||
isolated: bool,
|
|
||||||
preview: PreviewMode,
|
|
||||||
cache: &Cache,
|
|
||||||
printer: Printer,
|
|
||||||
) -> Result<RunEnvironment> {
|
|
||||||
let current_venv = if isolated {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
// Find the active environment if it exists
|
|
||||||
match PythonEnvironment::from_virtualenv(cache) {
|
|
||||||
Ok(env) => Some(env),
|
|
||||||
Err(uv_interpreter::Error::VenvNotFound) => None,
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO(zanieb): Support client configuration
|
|
||||||
let client_builder = BaseClientBuilder::default();
|
|
||||||
|
|
||||||
// Read all requirements from the provided sources.
|
|
||||||
// TODO(zanieb): Consider allowing constraints and extras
|
|
||||||
// TODO(zanieb): Allow specifying extras somehow
|
|
||||||
let spec = RequirementsSpecification::from_sources(
|
|
||||||
requirements,
|
|
||||||
&[],
|
|
||||||
overrides,
|
|
||||||
&ExtrasSpecification::None,
|
|
||||||
&client_builder,
|
|
||||||
preview,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Determine an interpreter to use
|
|
||||||
let python_env = if let Some(python) = python {
|
|
||||||
PythonEnvironment::from_requested_python(python, cache)?
|
|
||||||
} else {
|
|
||||||
PythonEnvironment::from_default_python(cache)?
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if the current environment satisfies the requirements
|
|
||||||
if let Some(venv) = current_venv {
|
|
||||||
// Ensure it matches the selected interpreter
|
|
||||||
// TODO(zanieb): We should check if a version was requested and see if the environment meets that
|
|
||||||
// too but this can wait until we refactor interpreter discovery
|
|
||||||
if venv.root() == python_env.root() {
|
|
||||||
// Determine the set of installed packages.
|
|
||||||
let site_packages = SitePackages::from_executable(&venv)?;
|
|
||||||
|
|
||||||
// If the requirements are already satisfied, we're done. Ideally, the resolver would be fast
|
|
||||||
// enough to let us remove this check. But right now, for large environments, it's an order of
|
|
||||||
// magnitude faster to validate the environment than to resolve the requirements.
|
|
||||||
if spec.source_trees.is_empty() {
|
|
||||||
match site_packages.satisfies(
|
|
||||||
&spec.requirements,
|
|
||||||
&spec.editables,
|
|
||||||
&spec.constraints,
|
|
||||||
)? {
|
|
||||||
SatisfiesResult::Fresh {
|
|
||||||
recursive_requirements,
|
|
||||||
} => {
|
|
||||||
debug!(
|
|
||||||
"All requirements satisfied: {}",
|
|
||||||
recursive_requirements
|
|
||||||
.iter()
|
|
||||||
.map(|entry| entry.requirement.to_string())
|
|
||||||
.sorted()
|
|
||||||
.join(" | ")
|
|
||||||
);
|
|
||||||
debug!(
|
|
||||||
"All editables satisfied: {}",
|
|
||||||
spec.editables.iter().map(ToString::to_string).join(", ")
|
|
||||||
);
|
|
||||||
return Ok(RunEnvironment {
|
|
||||||
python: venv,
|
|
||||||
_temp_dir_drop: None,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
SatisfiesResult::Unsatisfied(requirement) => {
|
|
||||||
debug!("At least one requirement is not satisfied: {requirement}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise, we need a new 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)?;
|
|
||||||
let tmpdir = tempdir_in(uv_state_path)?;
|
|
||||||
let venv = uv_virtualenv::create_venv(
|
|
||||||
tmpdir.path(),
|
|
||||||
python_env.into_interpreter(),
|
|
||||||
uv_virtualenv::Prompt::None,
|
|
||||||
false,
|
|
||||||
false,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Determine the tags, markers, and interpreter to use for resolution.
|
|
||||||
let interpreter = venv.interpreter().clone();
|
|
||||||
let tags = venv.interpreter().tags()?;
|
|
||||||
let markers = venv.interpreter().markers();
|
|
||||||
|
|
||||||
// Collect the set of required hashes.
|
|
||||||
// TODO(zanieb): Support hash checking
|
|
||||||
let hasher = HashStrategy::None;
|
|
||||||
|
|
||||||
// TODO(zanieb): Support index url configs
|
|
||||||
let index_locations = IndexLocations::default();
|
|
||||||
|
|
||||||
// TODO(zanieb): Support client options e.g. offline, tls, etc.
|
|
||||||
// Initialize the registry client.
|
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
|
||||||
.markers(markers)
|
|
||||||
.platform(interpreter.platform())
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// TODO(zanieb): Consider support for find links
|
|
||||||
let flat_index = FlatIndex::default();
|
|
||||||
|
|
||||||
// TODO(zanieb): Consider support for shared builds
|
|
||||||
// Determine whether to enable build isolation.
|
|
||||||
let build_isolation = BuildIsolation::Isolated;
|
|
||||||
|
|
||||||
// TODO(zanieb): Consider no-binary and no-build support
|
|
||||||
let no_build = NoBuild::None;
|
|
||||||
let no_binary = NoBinary::None;
|
|
||||||
|
|
||||||
// Create a shared in-memory index.
|
|
||||||
let index = InMemoryIndex::default();
|
|
||||||
|
|
||||||
// Track in-flight downloads, builds, etc., across resolutions.
|
|
||||||
let in_flight = InFlight::default();
|
|
||||||
|
|
||||||
let link_mode = LinkMode::default();
|
|
||||||
let config_settings = ConfigSettings::default();
|
|
||||||
|
|
||||||
// Create a build dispatch.
|
|
||||||
let build_dispatch = BuildDispatch::new(
|
|
||||||
&client,
|
|
||||||
cache,
|
|
||||||
&interpreter,
|
|
||||||
&index_locations,
|
|
||||||
&flat_index,
|
|
||||||
&index,
|
|
||||||
&in_flight,
|
|
||||||
SetupPyStrategy::default(),
|
|
||||||
&config_settings,
|
|
||||||
build_isolation,
|
|
||||||
link_mode,
|
|
||||||
&no_build,
|
|
||||||
&no_binary,
|
|
||||||
);
|
|
||||||
// TODO(zanieb): Consider `exclude-newer` support
|
|
||||||
|
|
||||||
// Resolve the requirements from the provided sources.
|
|
||||||
let requirements = {
|
|
||||||
// Convert from unnamed to named requirements.
|
|
||||||
let mut requirements = NamedRequirementsResolver::new(
|
|
||||||
spec.requirements,
|
|
||||||
&hasher,
|
|
||||||
&build_dispatch,
|
|
||||||
&client,
|
|
||||||
&index,
|
|
||||||
)
|
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
|
||||||
.resolve()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Resolve any source trees into requirements.
|
|
||||||
if !spec.source_trees.is_empty() {
|
|
||||||
requirements.extend(
|
|
||||||
SourceTreeResolver::new(
|
|
||||||
spec.source_trees,
|
|
||||||
&ExtrasSpecification::None,
|
|
||||||
&hasher,
|
|
||||||
&build_dispatch,
|
|
||||||
&client,
|
|
||||||
&index,
|
|
||||||
)
|
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
|
||||||
.resolve()
|
|
||||||
.await?,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
requirements
|
|
||||||
};
|
|
||||||
|
|
||||||
let options = OptionsBuilder::new()
|
|
||||||
// TODO(zanieb): Support resolver options
|
|
||||||
// .resolution_mode(resolution_mode)
|
|
||||||
// .prerelease_mode(prerelease_mode)
|
|
||||||
// .dependency_mode(dependency_mode)
|
|
||||||
// .exclude_newer(exclude_newer)
|
|
||||||
.build();
|
|
||||||
|
|
||||||
// Resolve the requirements.
|
|
||||||
let resolution = match resolve(
|
|
||||||
requirements,
|
|
||||||
spec.project,
|
|
||||||
&hasher,
|
|
||||||
&interpreter,
|
|
||||||
tags,
|
|
||||||
markers,
|
|
||||||
&client,
|
|
||||||
&flat_index,
|
|
||||||
&index,
|
|
||||||
&build_dispatch,
|
|
||||||
options,
|
|
||||||
printer,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(resolution) => Resolution::from(resolution),
|
|
||||||
Err(err) => return Err(err.into()),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Re-initialize the in-flight map.
|
|
||||||
let in_flight = InFlight::default();
|
|
||||||
|
|
||||||
// Sync the environment.
|
|
||||||
install(
|
|
||||||
&resolution,
|
|
||||||
SitePackages::from_executable(&venv)?,
|
|
||||||
&no_binary,
|
|
||||||
link_mode,
|
|
||||||
&index_locations,
|
|
||||||
&hasher,
|
|
||||||
tags,
|
|
||||||
&client,
|
|
||||||
&in_flight,
|
|
||||||
&build_dispatch,
|
|
||||||
cache,
|
|
||||||
&venv,
|
|
||||||
printer,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok(RunEnvironment {
|
|
||||||
python: venv,
|
|
||||||
_temp_dir_drop: Some(tmpdir),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a set of requirements, similar to running `pip compile`.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn resolve(
|
|
||||||
requirements: Vec<Requirement>,
|
|
||||||
project: Option<PackageName>,
|
|
||||||
hasher: &HashStrategy,
|
|
||||||
interpreter: &Interpreter,
|
|
||||||
tags: &Tags,
|
|
||||||
markers: &MarkerEnvironment,
|
|
||||||
client: &RegistryClient,
|
|
||||||
flat_index: &FlatIndex,
|
|
||||||
index: &InMemoryIndex,
|
|
||||||
build_dispatch: &BuildDispatch<'_>,
|
|
||||||
options: Options,
|
|
||||||
printer: Printer,
|
|
||||||
) -> Result<ResolutionGraph, Error> {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let exclusions = Exclusions::None;
|
|
||||||
let preferences = Vec::new();
|
|
||||||
let constraints = Constraints::default();
|
|
||||||
let overrides = Overrides::default();
|
|
||||||
let editables = Vec::new();
|
|
||||||
let installed_packages = EmptyInstalledPackages;
|
|
||||||
|
|
||||||
// Determine any lookahead requirements.
|
|
||||||
let lookaheads = LookaheadResolver::new(
|
|
||||||
&requirements,
|
|
||||||
&constraints,
|
|
||||||
&overrides,
|
|
||||||
&editables,
|
|
||||||
hasher,
|
|
||||||
build_dispatch,
|
|
||||||
client,
|
|
||||||
index,
|
|
||||||
)
|
|
||||||
.with_reporter(ResolverReporter::from(printer))
|
|
||||||
.resolve(markers)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Create a manifest of the requirements.
|
|
||||||
let manifest = Manifest::new(
|
|
||||||
requirements,
|
|
||||||
constraints,
|
|
||||||
overrides,
|
|
||||||
preferences,
|
|
||||||
project,
|
|
||||||
editables,
|
|
||||||
exclusions,
|
|
||||||
lookaheads,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Resolve the dependencies.
|
|
||||||
let resolver = Resolver::new(
|
|
||||||
manifest,
|
|
||||||
options,
|
|
||||||
markers,
|
|
||||||
interpreter,
|
|
||||||
tags,
|
|
||||||
client,
|
|
||||||
flat_index,
|
|
||||||
index,
|
|
||||||
hasher,
|
|
||||||
build_dispatch,
|
|
||||||
&installed_packages,
|
|
||||||
)?
|
|
||||||
.with_reporter(ResolverReporter::from(printer));
|
|
||||||
let resolution = resolver.resolve().await?;
|
|
||||||
|
|
||||||
let s = if resolution.len() == 1 { "" } else { "s" };
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}",
|
|
||||||
format!(
|
|
||||||
"Resolved {} in {}",
|
|
||||||
format!("{} package{}", resolution.len(), s).bold(),
|
|
||||||
elapsed(start.elapsed())
|
|
||||||
)
|
|
||||||
.dimmed()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
// Notify the user of any diagnostics.
|
|
||||||
for diagnostic in resolution.diagnostics() {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}{} {}",
|
|
||||||
"warning".yellow().bold(),
|
|
||||||
":".bold(),
|
|
||||||
diagnostic.message().bold()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(resolution)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Install a set of requirements into the current environment.
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
|
||||||
async fn install(
|
|
||||||
resolution: &Resolution,
|
|
||||||
site_packages: SitePackages<'_>,
|
|
||||||
no_binary: &NoBinary,
|
|
||||||
link_mode: LinkMode,
|
|
||||||
index_urls: &IndexLocations,
|
|
||||||
hasher: &HashStrategy,
|
|
||||||
tags: &Tags,
|
|
||||||
client: &RegistryClient,
|
|
||||||
in_flight: &InFlight,
|
|
||||||
build_dispatch: &BuildDispatch<'_>,
|
|
||||||
cache: &Cache,
|
|
||||||
venv: &PythonEnvironment,
|
|
||||||
printer: Printer,
|
|
||||||
) -> Result<(), Error> {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
|
|
||||||
let requirements = resolution.requirements();
|
|
||||||
|
|
||||||
// Partition into those that should be linked from the cache (`local`), those that need to be
|
|
||||||
// downloaded (`remote`), and those that should be removed (`extraneous`).
|
|
||||||
let plan = Planner::with_requirements(&requirements)
|
|
||||||
.build(
|
|
||||||
site_packages,
|
|
||||||
&Reinstall::None,
|
|
||||||
no_binary,
|
|
||||||
hasher,
|
|
||||||
index_urls,
|
|
||||||
cache,
|
|
||||||
venv,
|
|
||||||
tags,
|
|
||||||
)
|
|
||||||
.context("Failed to determine installation plan")?;
|
|
||||||
|
|
||||||
let Plan {
|
|
||||||
cached,
|
|
||||||
remote,
|
|
||||||
reinstalls,
|
|
||||||
installed: _,
|
|
||||||
extraneous: _,
|
|
||||||
} = plan;
|
|
||||||
|
|
||||||
// Nothing to do.
|
|
||||||
if remote.is_empty() && cached.is_empty() {
|
|
||||||
let s = if resolution.len() == 1 { "" } else { "s" };
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}",
|
|
||||||
format!(
|
|
||||||
"Audited {} in {}",
|
|
||||||
format!("{} package{}", resolution.len(), s).bold(),
|
|
||||||
elapsed(start.elapsed())
|
|
||||||
)
|
|
||||||
.dimmed()
|
|
||||||
)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map any registry-based requirements back to those returned by the resolver.
|
|
||||||
let remote = remote
|
|
||||||
.iter()
|
|
||||||
.map(|dist| {
|
|
||||||
resolution
|
|
||||||
.get_remote(&dist.name)
|
|
||||||
.cloned()
|
|
||||||
.expect("Resolution should contain all packages")
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// Download, build, and unzip any missing distributions.
|
|
||||||
let wheels = if remote.is_empty() {
|
|
||||||
vec![]
|
|
||||||
} else {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
|
|
||||||
let downloader = Downloader::new(cache, tags, hasher, client, build_dispatch)
|
|
||||||
.with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64));
|
|
||||||
|
|
||||||
let wheels = downloader
|
|
||||||
.download(remote.clone(), in_flight)
|
|
||||||
.await
|
|
||||||
.context("Failed to download distributions")?;
|
|
||||||
|
|
||||||
let s = if wheels.len() == 1 { "" } else { "s" };
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}",
|
|
||||||
format!(
|
|
||||||
"Downloaded {} in {}",
|
|
||||||
format!("{} package{}", wheels.len(), s).bold(),
|
|
||||||
elapsed(start.elapsed())
|
|
||||||
)
|
|
||||||
.dimmed()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
wheels
|
|
||||||
};
|
|
||||||
|
|
||||||
// Install the resolved distributions.
|
|
||||||
let wheels = wheels.into_iter().chain(cached).collect::<Vec<_>>();
|
|
||||||
if !wheels.is_empty() {
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
uv_installer::Installer::new(venv)
|
|
||||||
.with_link_mode(link_mode)
|
|
||||||
.with_reporter(InstallReporter::from(printer).with_length(wheels.len() as u64))
|
|
||||||
.install(&wheels)?;
|
|
||||||
|
|
||||||
let s = if wheels.len() == 1 { "" } else { "s" };
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}",
|
|
||||||
format!(
|
|
||||||
"Installed {} in {}",
|
|
||||||
format!("{} package{}", wheels.len(), s).bold(),
|
|
||||||
elapsed(start.elapsed())
|
|
||||||
)
|
|
||||||
.dimmed()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
for event in reinstalls
|
|
||||||
.into_iter()
|
|
||||||
.map(|distribution| ChangeEvent {
|
|
||||||
dist: LocalDist::from(distribution),
|
|
||||||
kind: ChangeEventKind::Removed,
|
|
||||||
})
|
|
||||||
.chain(wheels.into_iter().map(|distribution| ChangeEvent {
|
|
||||||
dist: LocalDist::from(distribution),
|
|
||||||
kind: ChangeEventKind::Added,
|
|
||||||
}))
|
|
||||||
.sorted_unstable_by(|a, b| {
|
|
||||||
a.dist
|
|
||||||
.name()
|
|
||||||
.cmp(b.dist.name())
|
|
||||||
.then_with(|| a.kind.cmp(&b.kind))
|
|
||||||
.then_with(|| a.dist.installed_version().cmp(&b.dist.installed_version()))
|
|
||||||
})
|
|
||||||
{
|
|
||||||
match event.kind {
|
|
||||||
ChangeEventKind::Added => {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
" {} {}{}",
|
|
||||||
"+".green(),
|
|
||||||
event.dist.name().as_ref().bold(),
|
|
||||||
event.dist.installed_version().to_string().dimmed()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
ChangeEventKind::Removed => {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
" {} {}{}",
|
|
||||||
"-".red(),
|
|
||||||
event.dist.name().as_ref().bold(),
|
|
||||||
event.dist.installed_version().to_string().dimmed()
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(konstin): Also check the cache whether any cached or installed dist is already known to
|
|
||||||
// have been yanked, we currently don't show this message on the second run anymore
|
|
||||||
for dist in &remote {
|
|
||||||
let Some(file) = dist.file() else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
match &file.yanked {
|
|
||||||
None | Some(Yanked::Bool(false)) => {}
|
|
||||||
Some(Yanked::Bool(true)) => {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}{} {dist} is yanked.",
|
|
||||||
"warning".yellow().bold(),
|
|
||||||
":".bold(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Some(Yanked::Reason(reason)) => {
|
|
||||||
writeln!(
|
|
||||||
printer.stderr(),
|
|
||||||
"{}{} {dist} is yanked (reason: \"{reason}\").",
|
|
||||||
"warning".yellow().bold(),
|
|
||||||
":".bold(),
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
|
||||||
enum Error {
|
|
||||||
#[error(transparent)]
|
|
||||||
Resolve(#[from] uv_resolver::ResolveError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Client(#[from] uv_client::Error),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Platform(#[from] platform_tags::PlatformError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Hash(#[from] uv_types::HashStrategyError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Io(#[from] std::io::Error),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Fmt(#[from] std::fmt::Error),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Lookahead(#[from] uv_requirements::LookaheadError),
|
|
||||||
|
|
||||||
#[error(transparent)]
|
|
||||||
Anyhow(#[from] anyhow::Error),
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,139 @@
|
||||||
|
use anstream::eprint;
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use distribution_types::IndexLocations;
|
||||||
|
use install_wheel_rs::linker::LinkMode;
|
||||||
|
use uv_cache::Cache;
|
||||||
|
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
|
||||||
|
use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy};
|
||||||
|
use uv_dispatch::BuildDispatch;
|
||||||
|
use uv_interpreter::PythonEnvironment;
|
||||||
|
use uv_requirements::{ExtrasSpecification, RequirementsSpecification};
|
||||||
|
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder};
|
||||||
|
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||||
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
|
use crate::commands::workspace::Error;
|
||||||
|
use crate::commands::{workspace, ExitStatus};
|
||||||
|
use crate::printer::Printer;
|
||||||
|
|
||||||
|
/// Resolve the project requirements into a lockfile.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) async fn lock(
|
||||||
|
preview: PreviewMode,
|
||||||
|
cache: &Cache,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<ExitStatus> {
|
||||||
|
if preview.is_disabled() {
|
||||||
|
warn_user!("`uv lock` is experimental and may change without warning.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(charlie): If the environment doesn't exist, create it.
|
||||||
|
let venv = PythonEnvironment::from_virtualenv(cache)?;
|
||||||
|
|
||||||
|
// Find the workspace requirements.
|
||||||
|
let Some(requirements) = workspace::find_workspace()? else {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Unable to find `pyproject.toml` for project workspace."
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(zanieb): Support client configuration
|
||||||
|
let client_builder = BaseClientBuilder::default();
|
||||||
|
|
||||||
|
// Read all requirements from the provided sources.
|
||||||
|
// TODO(zanieb): Consider allowing constraints and extras
|
||||||
|
// TODO(zanieb): Allow specifying extras somehow
|
||||||
|
let spec = RequirementsSpecification::from_sources(
|
||||||
|
&requirements,
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
&ExtrasSpecification::None,
|
||||||
|
&client_builder,
|
||||||
|
preview,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Determine the tags, markers, and interpreter to use for resolution.
|
||||||
|
let interpreter = venv.interpreter().clone();
|
||||||
|
let tags = venv.interpreter().tags()?;
|
||||||
|
let markers = venv.interpreter().markers();
|
||||||
|
|
||||||
|
// Initialize the registry client.
|
||||||
|
// TODO(zanieb): Support client options e.g. offline, tls, etc.
|
||||||
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
|
.markers(markers)
|
||||||
|
.platform(venv.interpreter().platform())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// TODO(charlie): Respect project configuration.
|
||||||
|
let build_isolation = BuildIsolation::default();
|
||||||
|
let config_settings = ConfigSettings::default();
|
||||||
|
let flat_index = FlatIndex::default();
|
||||||
|
let hasher = HashStrategy::default();
|
||||||
|
let in_flight = InFlight::default();
|
||||||
|
let index = InMemoryIndex::default();
|
||||||
|
let index_locations = IndexLocations::default();
|
||||||
|
let link_mode = LinkMode::default();
|
||||||
|
let no_binary = NoBinary::default();
|
||||||
|
let no_build = NoBuild::default();
|
||||||
|
let setup_py = SetupPyStrategy::default();
|
||||||
|
|
||||||
|
// Create a build dispatch.
|
||||||
|
let build_dispatch = BuildDispatch::new(
|
||||||
|
&client,
|
||||||
|
cache,
|
||||||
|
&interpreter,
|
||||||
|
&index_locations,
|
||||||
|
&flat_index,
|
||||||
|
&index,
|
||||||
|
&in_flight,
|
||||||
|
setup_py,
|
||||||
|
&config_settings,
|
||||||
|
build_isolation,
|
||||||
|
link_mode,
|
||||||
|
&no_build,
|
||||||
|
&no_binary,
|
||||||
|
);
|
||||||
|
|
||||||
|
let options = OptionsBuilder::new()
|
||||||
|
// TODO(zanieb): Support resolver options
|
||||||
|
// .resolution_mode(resolution_mode)
|
||||||
|
// .prerelease_mode(prerelease_mode)
|
||||||
|
// .dependency_mode(dependency_mode)
|
||||||
|
// .exclude_newer(exclude_newer)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Resolve the requirements.
|
||||||
|
let resolution = workspace::resolve(
|
||||||
|
spec,
|
||||||
|
&hasher,
|
||||||
|
&interpreter,
|
||||||
|
tags,
|
||||||
|
markers,
|
||||||
|
&client,
|
||||||
|
&flat_index,
|
||||||
|
&index,
|
||||||
|
&build_dispatch,
|
||||||
|
options,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let resolution = match resolution {
|
||||||
|
Err(Error::Resolve(uv_resolver::ResolveError::NoSolution(err))) => {
|
||||||
|
let report = miette::Report::msg(format!("{err}"))
|
||||||
|
.context("No solution found when resolving dependencies:");
|
||||||
|
eprint!("{report:?}");
|
||||||
|
return Ok(ExitStatus::Failure);
|
||||||
|
}
|
||||||
|
result => result,
|
||||||
|
}?;
|
||||||
|
|
||||||
|
// Write the lockfile to disk.
|
||||||
|
let lock = resolution.lock()?;
|
||||||
|
let encoded = toml::to_string_pretty(&lock)?;
|
||||||
|
fs_err::tokio::write("uv.lock", encoded.as_bytes()).await?;
|
||||||
|
|
||||||
|
Ok(ExitStatus::Success)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,401 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use itertools::Itertools;
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use distribution_types::{IndexLocations, InstalledMetadata, LocalDist, Name, Resolution};
|
||||||
|
use install_wheel_rs::linker::LinkMode;
|
||||||
|
use pep508_rs::MarkerEnvironment;
|
||||||
|
use platform_tags::Tags;
|
||||||
|
use pypi_types::Yanked;
|
||||||
|
use uv_cache::Cache;
|
||||||
|
use uv_client::RegistryClient;
|
||||||
|
use uv_configuration::{Constraints, NoBinary, Overrides, Reinstall};
|
||||||
|
use uv_dispatch::BuildDispatch;
|
||||||
|
use uv_fs::Simplified;
|
||||||
|
use uv_installer::{Downloader, Plan, Planner, SitePackages};
|
||||||
|
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||||
|
use uv_requirements::{
|
||||||
|
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
|
||||||
|
RequirementsSpecification, SourceTreeResolver,
|
||||||
|
};
|
||||||
|
use uv_resolver::{
|
||||||
|
Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, ResolutionGraph, Resolver,
|
||||||
|
};
|
||||||
|
use uv_types::{EmptyInstalledPackages, HashStrategy, InFlight};
|
||||||
|
|
||||||
|
use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter};
|
||||||
|
use crate::commands::{elapsed, ChangeEvent, ChangeEventKind};
|
||||||
|
use crate::printer::Printer;
|
||||||
|
|
||||||
|
pub(crate) mod lock;
|
||||||
|
pub(crate) mod run;
|
||||||
|
pub(crate) mod sync;
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub(crate) enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
Resolve(#[from] uv_resolver::ResolveError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Client(#[from] uv_client::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Platform(#[from] platform_tags::PlatformError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Hash(#[from] uv_types::HashStrategyError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Fmt(#[from] std::fmt::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Lookahead(#[from] uv_requirements::LookaheadError),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
Anyhow(#[from] anyhow::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Find the requirements for the current workspace.
|
||||||
|
pub(crate) fn find_workspace() -> Result<Option<Vec<RequirementsSource>>> {
|
||||||
|
// TODO(zanieb): Add/use workspace logic to load requirements for a workspace
|
||||||
|
// We cannot use `Workspace::find` yet because it depends on a `[tool.uv]` section
|
||||||
|
let pyproject_path = std::env::current_dir()?.join("pyproject.toml");
|
||||||
|
if pyproject_path.exists() {
|
||||||
|
debug!(
|
||||||
|
"Loading requirements from {}",
|
||||||
|
pyproject_path.user_display()
|
||||||
|
);
|
||||||
|
return Ok(Some(vec![
|
||||||
|
RequirementsSource::from_requirements_file(pyproject_path),
|
||||||
|
RequirementsSource::from_package(".".to_string()),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve a set of requirements, similar to running `pip compile`.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) async fn resolve(
|
||||||
|
spec: RequirementsSpecification,
|
||||||
|
hasher: &HashStrategy,
|
||||||
|
interpreter: &Interpreter,
|
||||||
|
tags: &Tags,
|
||||||
|
markers: &MarkerEnvironment,
|
||||||
|
client: &RegistryClient,
|
||||||
|
flat_index: &FlatIndex,
|
||||||
|
index: &InMemoryIndex,
|
||||||
|
build_dispatch: &BuildDispatch<'_>,
|
||||||
|
options: Options,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<ResolutionGraph, Error> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
let exclusions = Exclusions::None;
|
||||||
|
let preferences = Vec::new();
|
||||||
|
let constraints = Constraints::default();
|
||||||
|
let overrides = Overrides::default();
|
||||||
|
let editables = Vec::new();
|
||||||
|
let installed_packages = EmptyInstalledPackages;
|
||||||
|
|
||||||
|
// Resolve the requirements from the provided sources.
|
||||||
|
let requirements = {
|
||||||
|
// Convert from unnamed to named requirements.
|
||||||
|
let mut requirements = NamedRequirementsResolver::new(
|
||||||
|
spec.requirements,
|
||||||
|
hasher,
|
||||||
|
build_dispatch,
|
||||||
|
client,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
|
.resolve()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Resolve any source trees into requirements.
|
||||||
|
if !spec.source_trees.is_empty() {
|
||||||
|
requirements.extend(
|
||||||
|
SourceTreeResolver::new(
|
||||||
|
spec.source_trees,
|
||||||
|
&ExtrasSpecification::None,
|
||||||
|
hasher,
|
||||||
|
build_dispatch,
|
||||||
|
client,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
|
.resolve()
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
requirements
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine any lookahead requirements.
|
||||||
|
let lookaheads = LookaheadResolver::new(
|
||||||
|
&requirements,
|
||||||
|
&constraints,
|
||||||
|
&overrides,
|
||||||
|
&editables,
|
||||||
|
hasher,
|
||||||
|
build_dispatch,
|
||||||
|
client,
|
||||||
|
index,
|
||||||
|
)
|
||||||
|
.with_reporter(ResolverReporter::from(printer))
|
||||||
|
.resolve(markers)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Create a manifest of the requirements.
|
||||||
|
let manifest = Manifest::new(
|
||||||
|
requirements,
|
||||||
|
constraints,
|
||||||
|
overrides,
|
||||||
|
preferences,
|
||||||
|
spec.project,
|
||||||
|
editables,
|
||||||
|
exclusions,
|
||||||
|
lookaheads,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Resolve the dependencies.
|
||||||
|
let resolver = Resolver::new(
|
||||||
|
manifest,
|
||||||
|
options,
|
||||||
|
markers,
|
||||||
|
interpreter,
|
||||||
|
tags,
|
||||||
|
client,
|
||||||
|
flat_index,
|
||||||
|
index,
|
||||||
|
hasher,
|
||||||
|
build_dispatch,
|
||||||
|
&installed_packages,
|
||||||
|
)?
|
||||||
|
.with_reporter(ResolverReporter::from(printer));
|
||||||
|
let resolution = resolver.resolve().await?;
|
||||||
|
|
||||||
|
let s = if resolution.len() == 1 { "" } else { "s" };
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"Resolved {} in {}",
|
||||||
|
format!("{} package{}", resolution.len(), s).bold(),
|
||||||
|
elapsed(start.elapsed())
|
||||||
|
)
|
||||||
|
.dimmed()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Notify the user of any diagnostics.
|
||||||
|
for diagnostic in resolution.diagnostics() {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"{}{} {}",
|
||||||
|
"warning".yellow().bold(),
|
||||||
|
":".bold(),
|
||||||
|
diagnostic.message().bold()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(resolution)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install a set of requirements into the current environment.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) async fn install(
|
||||||
|
resolution: &Resolution,
|
||||||
|
site_packages: SitePackages<'_>,
|
||||||
|
no_binary: &NoBinary,
|
||||||
|
link_mode: LinkMode,
|
||||||
|
index_urls: &IndexLocations,
|
||||||
|
hasher: &HashStrategy,
|
||||||
|
tags: &Tags,
|
||||||
|
client: &RegistryClient,
|
||||||
|
in_flight: &InFlight,
|
||||||
|
build_dispatch: &BuildDispatch<'_>,
|
||||||
|
cache: &Cache,
|
||||||
|
venv: &PythonEnvironment,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
let requirements = resolution.requirements();
|
||||||
|
|
||||||
|
// Partition into those that should be linked from the cache (`local`), those that need to be
|
||||||
|
// downloaded (`remote`), and those that should be removed (`extraneous`).
|
||||||
|
let plan = Planner::with_requirements(&requirements)
|
||||||
|
.build(
|
||||||
|
site_packages,
|
||||||
|
&Reinstall::None,
|
||||||
|
no_binary,
|
||||||
|
hasher,
|
||||||
|
index_urls,
|
||||||
|
cache,
|
||||||
|
venv,
|
||||||
|
tags,
|
||||||
|
)
|
||||||
|
.context("Failed to determine installation plan")?;
|
||||||
|
|
||||||
|
let Plan {
|
||||||
|
cached,
|
||||||
|
remote,
|
||||||
|
reinstalls,
|
||||||
|
installed: _,
|
||||||
|
extraneous: _,
|
||||||
|
} = plan;
|
||||||
|
|
||||||
|
// Nothing to do.
|
||||||
|
if remote.is_empty() && cached.is_empty() {
|
||||||
|
let s = if resolution.len() == 1 { "" } else { "s" };
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"Audited {} in {}",
|
||||||
|
format!("{} package{}", resolution.len(), s).bold(),
|
||||||
|
elapsed(start.elapsed())
|
||||||
|
)
|
||||||
|
.dimmed()
|
||||||
|
)?;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map any registry-based requirements back to those returned by the resolver.
|
||||||
|
let remote = remote
|
||||||
|
.iter()
|
||||||
|
.map(|dist| {
|
||||||
|
resolution
|
||||||
|
.get_remote(&dist.name)
|
||||||
|
.cloned()
|
||||||
|
.expect("Resolution should contain all packages")
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
// Download, build, and unzip any missing distributions.
|
||||||
|
let wheels = if remote.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
|
let downloader = Downloader::new(cache, tags, hasher, client, build_dispatch)
|
||||||
|
.with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64));
|
||||||
|
|
||||||
|
let wheels = downloader
|
||||||
|
.download(remote.clone(), in_flight)
|
||||||
|
.await
|
||||||
|
.context("Failed to download distributions")?;
|
||||||
|
|
||||||
|
let s = if wheels.len() == 1 { "" } else { "s" };
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"Downloaded {} in {}",
|
||||||
|
format!("{} package{}", wheels.len(), s).bold(),
|
||||||
|
elapsed(start.elapsed())
|
||||||
|
)
|
||||||
|
.dimmed()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
wheels
|
||||||
|
};
|
||||||
|
|
||||||
|
// Install the resolved distributions.
|
||||||
|
let wheels = wheels.into_iter().chain(cached).collect::<Vec<_>>();
|
||||||
|
if !wheels.is_empty() {
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
uv_installer::Installer::new(venv)
|
||||||
|
.with_link_mode(link_mode)
|
||||||
|
.with_reporter(InstallReporter::from(printer).with_length(wheels.len() as u64))
|
||||||
|
.install(&wheels)?;
|
||||||
|
|
||||||
|
let s = if wheels.len() == 1 { "" } else { "s" };
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"{}",
|
||||||
|
format!(
|
||||||
|
"Installed {} in {}",
|
||||||
|
format!("{} package{}", wheels.len(), s).bold(),
|
||||||
|
elapsed(start.elapsed())
|
||||||
|
)
|
||||||
|
.dimmed()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for event in reinstalls
|
||||||
|
.into_iter()
|
||||||
|
.map(|distribution| ChangeEvent {
|
||||||
|
dist: LocalDist::from(distribution),
|
||||||
|
kind: ChangeEventKind::Removed,
|
||||||
|
})
|
||||||
|
.chain(wheels.into_iter().map(|distribution| ChangeEvent {
|
||||||
|
dist: LocalDist::from(distribution),
|
||||||
|
kind: ChangeEventKind::Added,
|
||||||
|
}))
|
||||||
|
.sorted_unstable_by(|a, b| {
|
||||||
|
a.dist
|
||||||
|
.name()
|
||||||
|
.cmp(b.dist.name())
|
||||||
|
.then_with(|| a.kind.cmp(&b.kind))
|
||||||
|
.then_with(|| a.dist.installed_version().cmp(&b.dist.installed_version()))
|
||||||
|
})
|
||||||
|
{
|
||||||
|
match event.kind {
|
||||||
|
ChangeEventKind::Added => {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
" {} {}{}",
|
||||||
|
"+".green(),
|
||||||
|
event.dist.name().as_ref().bold(),
|
||||||
|
event.dist.installed_version().to_string().dimmed()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
ChangeEventKind::Removed => {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
" {} {}{}",
|
||||||
|
"-".red(),
|
||||||
|
event.dist.name().as_ref().bold(),
|
||||||
|
event.dist.installed_version().to_string().dimmed()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(konstin): Also check the cache whether any cached or installed dist is already known to
|
||||||
|
// have been yanked, we currently don't show this message on the second run anymore
|
||||||
|
for dist in &remote {
|
||||||
|
let Some(file) = dist.file() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
match &file.yanked {
|
||||||
|
None | Some(Yanked::Bool(false)) => {}
|
||||||
|
Some(Yanked::Bool(true)) => {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"{}{} {dist} is yanked.",
|
||||||
|
"warning".yellow().bold(),
|
||||||
|
":".bold(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Some(Yanked::Reason(reason)) => {
|
||||||
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
"{}{} {dist} is yanked (reason: \"{reason}\").",
|
||||||
|
"warning".yellow().bold(),
|
||||||
|
":".bold(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,340 @@
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::{env, iter};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use itertools::Itertools;
|
||||||
|
use owo_colors::OwoColorize;
|
||||||
|
use tempfile::{tempdir_in, TempDir};
|
||||||
|
use tokio::process::Command;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
use distribution_types::{IndexLocations, Resolution};
|
||||||
|
use install_wheel_rs::linker::LinkMode;
|
||||||
|
use uv_cache::Cache;
|
||||||
|
use uv_client::{BaseClientBuilder, RegistryClientBuilder};
|
||||||
|
use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy};
|
||||||
|
use uv_dispatch::BuildDispatch;
|
||||||
|
use uv_fs::Simplified;
|
||||||
|
use uv_installer::{SatisfiesResult, SitePackages};
|
||||||
|
use uv_interpreter::PythonEnvironment;
|
||||||
|
use uv_requirements::{ExtrasSpecification, RequirementsSource, RequirementsSpecification};
|
||||||
|
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder};
|
||||||
|
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||||
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
|
use crate::commands::{workspace, ExitStatus};
|
||||||
|
use crate::printer::Printer;
|
||||||
|
|
||||||
|
/// Run a command.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) async fn run(
|
||||||
|
target: Option<String>,
|
||||||
|
mut args: Vec<OsString>,
|
||||||
|
mut requirements: Vec<RequirementsSource>,
|
||||||
|
python: Option<String>,
|
||||||
|
isolated: bool,
|
||||||
|
preview: PreviewMode,
|
||||||
|
cache: &Cache,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<ExitStatus> {
|
||||||
|
if preview.is_disabled() {
|
||||||
|
warn_user!("`uv run` is experimental and may change without warning.");
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Copy the requirements into a set of overrides; we'll use this to prioritize
|
||||||
|
// requested requirements over those discovered in the project.
|
||||||
|
// We must retain these requirements as direct dependencies too, as overrides
|
||||||
|
// cannot be applied to transitive dependencies.
|
||||||
|
let overrides = requirements.clone();
|
||||||
|
|
||||||
|
if !isolated {
|
||||||
|
if let Some(workspace_requirements) = workspace::find_workspace()? {
|
||||||
|
requirements.extend(workspace_requirements);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect the current Python interpreter.
|
||||||
|
// TODO(zanieb): Create ephemeral environments
|
||||||
|
// TODO(zanieb): Accept `--python`
|
||||||
|
let run_env = environment_for_run(
|
||||||
|
&requirements,
|
||||||
|
&overrides,
|
||||||
|
python.as_deref(),
|
||||||
|
isolated,
|
||||||
|
preview,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
let python_env = run_env.python;
|
||||||
|
|
||||||
|
// Construct the command
|
||||||
|
let mut process = Command::new(&command);
|
||||||
|
process.args(&args);
|
||||||
|
|
||||||
|
// Set up the PATH
|
||||||
|
debug!(
|
||||||
|
"Using Python {} environment at {}",
|
||||||
|
python_env.interpreter().python_version(),
|
||||||
|
python_env.python_executable().user_display().cyan()
|
||||||
|
);
|
||||||
|
let new_path = if let Some(path) = env::var_os("PATH") {
|
||||||
|
let python_env_path =
|
||||||
|
iter::once(python_env.scripts().to_path_buf()).chain(env::split_paths(&path));
|
||||||
|
env::join_paths(python_env_path)?
|
||||||
|
} else {
|
||||||
|
OsString::from(python_env.scripts())
|
||||||
|
};
|
||||||
|
|
||||||
|
process.env("PATH", new_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()?;
|
||||||
|
let status = handle.wait().await?;
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct RunEnvironment {
|
||||||
|
/// The Python environment to execute the run in.
|
||||||
|
python: PythonEnvironment,
|
||||||
|
/// A temporary directory, if a new virtual environment was created.
|
||||||
|
///
|
||||||
|
/// Included to ensure that the temporary directory exists for the length of the operation, but
|
||||||
|
/// is dropped at the end as appropriate.
|
||||||
|
_temp_dir_drop: Option<TempDir>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns an environment for a `run` invocation.
|
||||||
|
///
|
||||||
|
/// Will use the current virtual environment (if any) unless `isolated` is true.
|
||||||
|
/// Will create virtual environments in a temporary directory (if necessary).
|
||||||
|
async fn environment_for_run(
|
||||||
|
requirements: &[RequirementsSource],
|
||||||
|
overrides: &[RequirementsSource],
|
||||||
|
python: Option<&str>,
|
||||||
|
isolated: bool,
|
||||||
|
preview: PreviewMode,
|
||||||
|
cache: &Cache,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<RunEnvironment> {
|
||||||
|
let current_venv = if isolated {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// Find the active environment if it exists
|
||||||
|
match PythonEnvironment::from_virtualenv(cache) {
|
||||||
|
Ok(env) => Some(env),
|
||||||
|
Err(uv_interpreter::Error::VenvNotFound) => None,
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO(zanieb): Support client configuration
|
||||||
|
let client_builder = BaseClientBuilder::default();
|
||||||
|
|
||||||
|
// Read all requirements from the provided sources.
|
||||||
|
// TODO(zanieb): Consider allowing constraints and extras
|
||||||
|
// TODO(zanieb): Allow specifying extras somehow
|
||||||
|
let spec = RequirementsSpecification::from_sources(
|
||||||
|
requirements,
|
||||||
|
&[],
|
||||||
|
overrides,
|
||||||
|
&ExtrasSpecification::None,
|
||||||
|
&client_builder,
|
||||||
|
preview,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Determine an interpreter to use
|
||||||
|
let python_env = if let Some(python) = python {
|
||||||
|
PythonEnvironment::from_requested_python(python, cache)?
|
||||||
|
} else {
|
||||||
|
PythonEnvironment::from_default_python(cache)?
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if the current environment satisfies the requirements
|
||||||
|
if let Some(venv) = current_venv {
|
||||||
|
// Ensure it matches the selected interpreter
|
||||||
|
// TODO(zanieb): We should check if a version was requested and see if the environment meets that
|
||||||
|
// too but this can wait until we refactor interpreter discovery
|
||||||
|
if venv.root() == python_env.root() {
|
||||||
|
// Determine the set of installed packages.
|
||||||
|
let site_packages = SitePackages::from_executable(&venv)?;
|
||||||
|
|
||||||
|
// If the requirements are already satisfied, we're done. Ideally, the resolver would be fast
|
||||||
|
// enough to let us remove this check. But right now, for large environments, it's an order of
|
||||||
|
// magnitude faster to validate the environment than to resolve the requirements.
|
||||||
|
if spec.source_trees.is_empty() {
|
||||||
|
match site_packages.satisfies(
|
||||||
|
&spec.requirements,
|
||||||
|
&spec.editables,
|
||||||
|
&spec.constraints,
|
||||||
|
)? {
|
||||||
|
SatisfiesResult::Fresh {
|
||||||
|
recursive_requirements,
|
||||||
|
} => {
|
||||||
|
debug!(
|
||||||
|
"All requirements satisfied: {}",
|
||||||
|
recursive_requirements
|
||||||
|
.iter()
|
||||||
|
.map(|entry| entry.requirement.to_string())
|
||||||
|
.sorted()
|
||||||
|
.join(" | ")
|
||||||
|
);
|
||||||
|
debug!(
|
||||||
|
"All editables satisfied: {}",
|
||||||
|
spec.editables.iter().map(ToString::to_string).join(", ")
|
||||||
|
);
|
||||||
|
return Ok(RunEnvironment {
|
||||||
|
python: venv,
|
||||||
|
_temp_dir_drop: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
SatisfiesResult::Unsatisfied(requirement) => {
|
||||||
|
debug!("At least one requirement is not satisfied: {requirement}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Otherwise, we need a new environment
|
||||||
|
|
||||||
|
// Create a virtual environment
|
||||||
|
// TODO(zanieb): Move this path derivation elsewhere
|
||||||
|
let uv_state_path = env::current_dir()?.join(".uv");
|
||||||
|
fs_err::create_dir_all(&uv_state_path)?;
|
||||||
|
let tmpdir = tempdir_in(uv_state_path)?;
|
||||||
|
let venv = uv_virtualenv::create_venv(
|
||||||
|
tmpdir.path(),
|
||||||
|
python_env.into_interpreter(),
|
||||||
|
uv_virtualenv::Prompt::None,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Determine the tags, markers, and interpreter to use for resolution.
|
||||||
|
let interpreter = venv.interpreter().clone();
|
||||||
|
let tags = venv.interpreter().tags()?;
|
||||||
|
let markers = venv.interpreter().markers();
|
||||||
|
|
||||||
|
// Initialize the registry client.
|
||||||
|
// TODO(zanieb): Support client options e.g. offline, tls, etc.
|
||||||
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
|
.markers(markers)
|
||||||
|
.platform(venv.interpreter().platform())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// TODO(charlie): Respect project configuration.
|
||||||
|
let build_isolation = BuildIsolation::default();
|
||||||
|
let config_settings = ConfigSettings::default();
|
||||||
|
let flat_index = FlatIndex::default();
|
||||||
|
let hasher = HashStrategy::default();
|
||||||
|
let in_flight = InFlight::default();
|
||||||
|
let index = InMemoryIndex::default();
|
||||||
|
let index_locations = IndexLocations::default();
|
||||||
|
let link_mode = LinkMode::default();
|
||||||
|
let no_binary = NoBinary::default();
|
||||||
|
let no_build = NoBuild::default();
|
||||||
|
let setup_py = SetupPyStrategy::default();
|
||||||
|
|
||||||
|
// Create a build dispatch.
|
||||||
|
let build_dispatch = BuildDispatch::new(
|
||||||
|
&client,
|
||||||
|
cache,
|
||||||
|
&interpreter,
|
||||||
|
&index_locations,
|
||||||
|
&flat_index,
|
||||||
|
&index,
|
||||||
|
&in_flight,
|
||||||
|
setup_py,
|
||||||
|
&config_settings,
|
||||||
|
build_isolation,
|
||||||
|
link_mode,
|
||||||
|
&no_build,
|
||||||
|
&no_binary,
|
||||||
|
);
|
||||||
|
|
||||||
|
let options = OptionsBuilder::new()
|
||||||
|
// TODO(zanieb): Support resolver options
|
||||||
|
// .resolution_mode(resolution_mode)
|
||||||
|
// .prerelease_mode(prerelease_mode)
|
||||||
|
// .dependency_mode(dependency_mode)
|
||||||
|
// .exclude_newer(exclude_newer)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Resolve the requirements.
|
||||||
|
let resolution = match workspace::resolve(
|
||||||
|
spec,
|
||||||
|
&hasher,
|
||||||
|
&interpreter,
|
||||||
|
tags,
|
||||||
|
markers,
|
||||||
|
&client,
|
||||||
|
&flat_index,
|
||||||
|
&index,
|
||||||
|
&build_dispatch,
|
||||||
|
options,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(resolution) => Resolution::from(resolution),
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Re-initialize the in-flight map.
|
||||||
|
let in_flight = InFlight::default();
|
||||||
|
|
||||||
|
// Sync the environment.
|
||||||
|
workspace::install(
|
||||||
|
&resolution,
|
||||||
|
SitePackages::from_executable(&venv)?,
|
||||||
|
&no_binary,
|
||||||
|
link_mode,
|
||||||
|
&index_locations,
|
||||||
|
&hasher,
|
||||||
|
tags,
|
||||||
|
&client,
|
||||||
|
&in_flight,
|
||||||
|
&build_dispatch,
|
||||||
|
cache,
|
||||||
|
&venv,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(RunEnvironment {
|
||||||
|
python: venv,
|
||||||
|
_temp_dir_drop: Some(tmpdir),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
use anyhow::Result;
|
||||||
|
|
||||||
|
use distribution_types::IndexLocations;
|
||||||
|
use install_wheel_rs::linker::LinkMode;
|
||||||
|
use uv_cache::Cache;
|
||||||
|
use uv_client::RegistryClientBuilder;
|
||||||
|
use uv_configuration::{ConfigSettings, NoBinary, NoBuild, PreviewMode, SetupPyStrategy};
|
||||||
|
use uv_dispatch::BuildDispatch;
|
||||||
|
use uv_installer::SitePackages;
|
||||||
|
use uv_interpreter::PythonEnvironment;
|
||||||
|
use uv_normalize::PackageName;
|
||||||
|
use uv_resolver::{FlatIndex, InMemoryIndex, Lock};
|
||||||
|
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||||
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
|
use crate::commands::{workspace, ExitStatus};
|
||||||
|
use crate::printer::Printer;
|
||||||
|
|
||||||
|
/// Sync the project environment.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub(crate) async fn sync(
|
||||||
|
preview: PreviewMode,
|
||||||
|
cache: &Cache,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<ExitStatus> {
|
||||||
|
if preview.is_disabled() {
|
||||||
|
warn_user!("`uv sync` is experimental and may change without warning.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(charlie): If the environment doesn't exist, create it.
|
||||||
|
let venv = PythonEnvironment::from_virtualenv(cache)?;
|
||||||
|
let markers = venv.interpreter().markers();
|
||||||
|
let tags = venv.interpreter().tags()?;
|
||||||
|
|
||||||
|
// Initialize the registry client.
|
||||||
|
// TODO(zanieb): Support client options e.g. offline, tls, etc.
|
||||||
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
|
.markers(markers)
|
||||||
|
.platform(venv.interpreter().platform())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// TODO(charlie): Respect project configuration.
|
||||||
|
let build_isolation = BuildIsolation::default();
|
||||||
|
let config_settings = ConfigSettings::default();
|
||||||
|
let flat_index = FlatIndex::default();
|
||||||
|
let hasher = HashStrategy::default();
|
||||||
|
let in_flight = InFlight::default();
|
||||||
|
let index = InMemoryIndex::default();
|
||||||
|
let index_locations = IndexLocations::default();
|
||||||
|
let link_mode = LinkMode::default();
|
||||||
|
let no_binary = NoBinary::default();
|
||||||
|
let no_build = NoBuild::default();
|
||||||
|
let setup_py = SetupPyStrategy::default();
|
||||||
|
|
||||||
|
// Create a build dispatch.
|
||||||
|
let build_dispatch = BuildDispatch::new(
|
||||||
|
&client,
|
||||||
|
cache,
|
||||||
|
venv.interpreter(),
|
||||||
|
&index_locations,
|
||||||
|
&flat_index,
|
||||||
|
&index,
|
||||||
|
&in_flight,
|
||||||
|
setup_py,
|
||||||
|
&config_settings,
|
||||||
|
build_isolation,
|
||||||
|
link_mode,
|
||||||
|
&no_build,
|
||||||
|
&no_binary,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Read the lockfile.
|
||||||
|
let resolution = {
|
||||||
|
// TODO(charlie): Read the project name from disk.
|
||||||
|
let root = PackageName::new("project".to_string())?;
|
||||||
|
let encoded = fs_err::tokio::read_to_string("uv.lock").await?;
|
||||||
|
let lock: Lock = toml::from_str(&encoded)?;
|
||||||
|
lock.to_resolution(markers, tags, &root)
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync the environment.
|
||||||
|
workspace::install(
|
||||||
|
&resolution,
|
||||||
|
SitePackages::from_executable(&venv)?,
|
||||||
|
&no_binary,
|
||||||
|
link_mode,
|
||||||
|
&index_locations,
|
||||||
|
&hasher,
|
||||||
|
tags,
|
||||||
|
&client,
|
||||||
|
&in_flight,
|
||||||
|
&build_dispatch,
|
||||||
|
cache,
|
||||||
|
&venv,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ExitStatus::Success)
|
||||||
|
}
|
||||||
|
|
@ -528,6 +528,18 @@ async fn run() -> Result<ExitStatus> {
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
Commands::Sync(args) => {
|
||||||
|
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||||
|
let _args = settings::SyncSettings::resolve(args, workspace);
|
||||||
|
|
||||||
|
commands::sync(globals.preview, &cache, printer).await
|
||||||
|
}
|
||||||
|
Commands::Lock(args) => {
|
||||||
|
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||||
|
let _args = settings::LockSettings::resolve(args, workspace);
|
||||||
|
|
||||||
|
commands::lock(globals.preview, &cache, printer).await
|
||||||
|
}
|
||||||
#[cfg(feature = "self-update")]
|
#[cfg(feature = "self-update")]
|
||||||
Commands::Self_(SelfNamespace {
|
Commands::Self_(SelfNamespace {
|
||||||
command: SelfCommand::Update,
|
command: SelfCommand::Update,
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,9 @@ use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode,
|
||||||
use uv_workspace::{PipOptions, Workspace};
|
use uv_workspace::{PipOptions, Workspace};
|
||||||
|
|
||||||
use crate::cli::{
|
use crate::cli::{
|
||||||
ColorChoice, GlobalArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs,
|
ColorChoice, GlobalArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs,
|
||||||
PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RunArgs, VenvArgs,
|
PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RunArgs, SyncArgs,
|
||||||
|
VenvArgs,
|
||||||
};
|
};
|
||||||
use crate::commands::ListFormat;
|
use crate::commands::ListFormat;
|
||||||
|
|
||||||
|
|
@ -110,6 +111,48 @@ impl RunSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The resolved settings to use for a `sync` invocation.
|
||||||
|
#[allow(clippy::struct_excessive_bools, dead_code)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct SyncSettings {
|
||||||
|
// CLI-only settings.
|
||||||
|
pub(crate) python: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SyncSettings {
|
||||||
|
/// Resolve the [`SyncSettings`] from the CLI and workspace configuration.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub(crate) fn resolve(args: SyncArgs, _workspace: Option<Workspace>) -> Self {
|
||||||
|
let SyncArgs { python } = args;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
// CLI-only settings.
|
||||||
|
python,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The resolved settings to use for a `lock` invocation.
|
||||||
|
#[allow(clippy::struct_excessive_bools, dead_code)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct LockSettings {
|
||||||
|
// CLI-only settings.
|
||||||
|
pub(crate) python: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LockSettings {
|
||||||
|
/// Resolve the [`LockSettings`] from the CLI and workspace configuration.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub(crate) fn resolve(args: LockArgs, _workspace: Option<Workspace>) -> Self {
|
||||||
|
let LockArgs { python } = args;
|
||||||
|
|
||||||
|
Self {
|
||||||
|
// CLI-only settings.
|
||||||
|
python,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The resolved settings to use for a `pip compile` invocation.
|
/// The resolved settings to use for a `pip compile` invocation.
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
||||||
|
|
@ -314,6 +314,24 @@ dependencies = ["flask==1.0.x"]
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_pip() {
|
||||||
|
uv_snapshot!(Command::new(get_bin()).arg("install"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: unrecognized subcommand 'install'
|
||||||
|
|
||||||
|
tip: a similar subcommand exists: 'uv pip install'
|
||||||
|
|
||||||
|
Usage: uv [OPTIONS] <COMMAND>
|
||||||
|
|
||||||
|
For more information, try '--help'.
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn no_solution() {
|
fn no_solution() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
|
||||||
|
|
@ -74,24 +74,6 @@ fn uninstall_command(context: &TestContext) -> Command {
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn missing_pip() {
|
|
||||||
uv_snapshot!(Command::new(get_bin()).arg("sync"), @r###"
|
|
||||||
success: false
|
|
||||||
exit_code: 2
|
|
||||||
----- stdout -----
|
|
||||||
|
|
||||||
----- stderr -----
|
|
||||||
error: unrecognized subcommand 'sync'
|
|
||||||
|
|
||||||
tip: a similar subcommand exists: 'uv pip sync'
|
|
||||||
|
|
||||||
Usage: uv [OPTIONS] <COMMAND>
|
|
||||||
|
|
||||||
For more information, try '--help'.
|
|
||||||
"###);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn missing_requirements_txt() {
|
fn missing_requirements_txt() {
|
||||||
let context = TestContext::new("3.12");
|
let context = TestContext::new("3.12");
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue