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:
Charlie Marsh 2024-05-08 10:51:51 -04:00 committed by GitHub
parent 0228b15baf
commit 76a3ceb2ca
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 1118 additions and 760 deletions

View File

@ -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),
} }

View File

@ -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,

View File

@ -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 {

View File

@ -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 {

View File

@ -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),
}

View File

@ -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)
}

View File

@ -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(())
}

View File

@ -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),
})
}

View File

@ -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)
}

View File

@ -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,

View File

@ -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)]

View File

@ -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");

View File

@ -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");