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;
|
||||
|
||||
/// Whether to enforce build isolation when building source distributions.
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
#[derive(Debug, Default, Copy, Clone)]
|
||||
pub enum BuildIsolation<'a> {
|
||||
#[default]
|
||||
Isolated,
|
||||
Shared(&'a PythonEnvironment),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ use pep508_rs::MarkerEnvironment;
|
|||
use pypi_types::{HashDigest, HashError};
|
||||
use uv_normalize::PackageName;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub enum HashStrategy {
|
||||
/// No hash policy is specified.
|
||||
#[default]
|
||||
None,
|
||||
/// Hashes should be generated (specifically, a SHA-256 hash), but not validated.
|
||||
Generate,
|
||||
|
|
|
|||
|
|
@ -133,8 +133,15 @@ pub(crate) enum Commands {
|
|||
/// Clear the cache, removing all entries or those linked to specific packages.
|
||||
#[command(hide = true)]
|
||||
Clean(CleanArgs),
|
||||
/// Run a command in the project environment.
|
||||
#[clap(hide = true)]
|
||||
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
|
||||
Version {
|
||||
#[arg(long, value_enum, default_value = "text")]
|
||||
|
|
@ -1852,6 +1859,54 @@ pub(crate) struct RunArgs {
|
|||
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)]
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
struct AddArgs {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ pub(crate) use pip_list::pip_list;
|
|||
pub(crate) use pip_show::pip_show;
|
||||
pub(crate) use pip_sync::pip_sync;
|
||||
pub(crate) use pip_uninstall::pip_uninstall;
|
||||
pub(crate) use run::run;
|
||||
#[cfg(feature = "self-update")]
|
||||
pub(crate) use self_update::self_update;
|
||||
use uv_cache::Cache;
|
||||
|
|
@ -26,6 +25,9 @@ use uv_interpreter::PythonEnvironment;
|
|||
use uv_normalize::PackageName;
|
||||
pub(crate) use venv::venv;
|
||||
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;
|
||||
|
||||
|
|
@ -41,11 +43,11 @@ mod pip_show;
|
|||
mod pip_sync;
|
||||
mod pip_uninstall;
|
||||
mod reporters;
|
||||
mod run;
|
||||
#[cfg(feature = "self-update")]
|
||||
mod self_update;
|
||||
mod venv;
|
||||
mod version;
|
||||
mod workspace;
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
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
|
||||
}
|
||||
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")]
|
||||
Commands::Self_(SelfNamespace {
|
||||
command: SelfCommand::Update,
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode,
|
|||
use uv_workspace::{PipOptions, Workspace};
|
||||
|
||||
use crate::cli::{
|
||||
ColorChoice, GlobalArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs, PipInstallArgs,
|
||||
PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RunArgs, VenvArgs,
|
||||
ColorChoice, GlobalArgs, LockArgs, Maybe, PipCheckArgs, PipCompileArgs, PipFreezeArgs,
|
||||
PipInstallArgs, PipListArgs, PipShowArgs, PipSyncArgs, PipUninstallArgs, RunArgs, SyncArgs,
|
||||
VenvArgs,
|
||||
};
|
||||
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.
|
||||
#[allow(clippy::struct_excessive_bools)]
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
|
|||
|
|
@ -314,6 +314,24 @@ dependencies = ["flask==1.0.x"]
|
|||
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]
|
||||
fn no_solution() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
|
|||
|
|
@ -74,24 +74,6 @@ fn uninstall_command(context: &TestContext) -> 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]
|
||||
fn missing_requirements_txt() {
|
||||
let context = TestContext::new("3.12");
|
||||
|
|
|
|||
Loading…
Reference in New Issue