mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Support unnamed requirements in uv tool install
(#4716)
## Summary This PR adds support for (e.g.) `uv tool install git+https://github.com/psf/black`. Closes https://github.com/astral-sh/uv/issues/4664.
This commit is contained in:
parent
368276d7d1
commit
8e935e2c17
10 changed files with 522 additions and 66 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -5040,7 +5040,6 @@ dependencies = [
|
||||||
"pathdiff",
|
"pathdiff",
|
||||||
"pep440_rs",
|
"pep440_rs",
|
||||||
"pep508_rs",
|
"pep508_rs",
|
||||||
"pypi-types",
|
|
||||||
"serde",
|
"serde",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
|
|
|
@ -314,6 +314,13 @@ impl RequirementsSpecification {
|
||||||
Ok(spec)
|
Ok(spec)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse an individual package requirement.
|
||||||
|
pub fn parse_package(name: &str) -> Result<UnresolvedRequirementSpecification> {
|
||||||
|
let requirement = RequirementsTxtRequirement::parse(name, std::env::current_dir()?, false)
|
||||||
|
.with_context(|| format!("Failed to parse: `{name}`"))?;
|
||||||
|
Ok(UnresolvedRequirementSpecification::from(requirement))
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the requirements from a set of sources.
|
/// Read the requirements from a set of sources.
|
||||||
pub async fn from_simple_sources(
|
pub async fn from_simple_sources(
|
||||||
requirements: &[RequirementsSource],
|
requirements: &[RequirementsSource],
|
||||||
|
|
|
@ -16,7 +16,6 @@ workspace = true
|
||||||
install-wheel-rs = { workspace = true }
|
install-wheel-rs = { workspace = true }
|
||||||
pep440_rs = { workspace = true }
|
pep440_rs = { workspace = true }
|
||||||
pep508_rs = { workspace = true }
|
pep508_rs = { workspace = true }
|
||||||
pypi-types = { workspace = true }
|
|
||||||
uv-cache = { workspace = true }
|
uv-cache = { workspace = true }
|
||||||
uv-fs = { workspace = true }
|
uv-fs = { workspace = true }
|
||||||
uv-state = { workspace = true }
|
uv-state = { workspace = true }
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use path_slash::PathBufExt;
|
use path_slash::PathBufExt;
|
||||||
use pypi_types::VerbatimParsedUrl;
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use toml_edit::value;
|
use toml_edit::value;
|
||||||
use toml_edit::Array;
|
use toml_edit::Array;
|
||||||
|
@ -14,11 +13,11 @@ use toml_edit::Value;
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||||
pub struct Tool {
|
pub struct Tool {
|
||||||
// The requirements requested by the user during installation.
|
/// The requirements requested by the user during installation.
|
||||||
requirements: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
|
requirements: Vec<pep508_rs::Requirement>,
|
||||||
/// The Python requested by the user during installation.
|
/// The Python requested by the user during installation.
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
// A mapping of entry point names to their metadata.
|
/// A mapping of entry point names to their metadata.
|
||||||
entrypoints: Vec<ToolEntrypoint>,
|
entrypoints: Vec<ToolEntrypoint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -59,7 +58,7 @@ fn each_element_on_its_line_array(elements: impl Iterator<Item = impl Into<Value
|
||||||
impl Tool {
|
impl Tool {
|
||||||
/// Create a new `Tool`.
|
/// Create a new `Tool`.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
requirements: Vec<pep508_rs::Requirement<VerbatimParsedUrl>>,
|
requirements: Vec<pep508_rs::Requirement>,
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
entrypoints: impl Iterator<Item = ToolEntrypoint>,
|
entrypoints: impl Iterator<Item = ToolEntrypoint>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
|
@ -4,17 +4,18 @@ use itertools::Itertools;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use distribution_types::Resolution;
|
use distribution_types::{Resolution, UnresolvedRequirementSpecification};
|
||||||
use pep440_rs::Version;
|
use pep440_rs::Version;
|
||||||
|
use pypi_types::Requirement;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder};
|
||||||
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode, SetupPyStrategy};
|
use uv_configuration::{Concurrency, ExtrasSpecification, PreviewMode, SetupPyStrategy};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
use uv_distribution::Workspace;
|
use uv_distribution::{DistributionDatabase, Workspace};
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_git::GitResolver;
|
use uv_git::GitResolver;
|
||||||
use uv_installer::{SatisfiesResult, SitePackages};
|
use uv_installer::{SatisfiesResult, SitePackages};
|
||||||
use uv_requirements::RequirementsSpecification;
|
use uv_requirements::{NamedRequirementsResolver, RequirementsSpecification};
|
||||||
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder, PythonRequirement, RequiresPython};
|
use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder, PythonRequirement, RequiresPython};
|
||||||
use uv_toolchain::{
|
use uv_toolchain::{
|
||||||
request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, Toolchain,
|
request_from_version_file, EnvironmentPreference, Interpreter, PythonEnvironment, Toolchain,
|
||||||
|
@ -23,6 +24,7 @@ use uv_toolchain::{
|
||||||
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
use uv_types::{BuildIsolation, HashStrategy, InFlight};
|
||||||
|
|
||||||
use crate::commands::pip;
|
use crate::commands::pip;
|
||||||
|
use crate::commands::reporters::ResolverReporter;
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
use crate::settings::ResolverInstallerSettings;
|
use crate::settings::ResolverInstallerSettings;
|
||||||
|
|
||||||
|
@ -282,6 +284,89 @@ pub(crate) struct SharedState {
|
||||||
index: InMemoryIndex,
|
index: InMemoryIndex,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve any [`UnresolvedRequirementSpecification`] into a fully-qualified [`Requirement`].
|
||||||
|
pub(crate) async fn resolve_names(
|
||||||
|
requirements: Vec<UnresolvedRequirementSpecification>,
|
||||||
|
interpreter: &Interpreter,
|
||||||
|
settings: &ResolverInstallerSettings,
|
||||||
|
state: &SharedState,
|
||||||
|
preview: PreviewMode,
|
||||||
|
connectivity: Connectivity,
|
||||||
|
concurrency: Concurrency,
|
||||||
|
native_tls: bool,
|
||||||
|
cache: &Cache,
|
||||||
|
printer: Printer,
|
||||||
|
) -> anyhow::Result<Vec<Requirement>> {
|
||||||
|
// Extract the project settings.
|
||||||
|
let ResolverInstallerSettings {
|
||||||
|
index_locations,
|
||||||
|
index_strategy,
|
||||||
|
keyring_provider,
|
||||||
|
resolution: _,
|
||||||
|
prerelease: _,
|
||||||
|
config_setting,
|
||||||
|
exclude_newer,
|
||||||
|
link_mode,
|
||||||
|
compile_bytecode: _,
|
||||||
|
upgrade: _,
|
||||||
|
reinstall: _,
|
||||||
|
build_options,
|
||||||
|
} = settings;
|
||||||
|
|
||||||
|
// Initialize the registry client.
|
||||||
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
|
.native_tls(native_tls)
|
||||||
|
.connectivity(connectivity)
|
||||||
|
.index_urls(index_locations.index_urls())
|
||||||
|
.index_strategy(*index_strategy)
|
||||||
|
.keyring(*keyring_provider)
|
||||||
|
.markers(interpreter.markers())
|
||||||
|
.platform(interpreter.platform())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// Initialize any shared state.
|
||||||
|
let in_flight = InFlight::default();
|
||||||
|
|
||||||
|
// TODO(charlie): These are all default values. We should consider whether we want to make them
|
||||||
|
// optional on the downstream APIs.
|
||||||
|
let build_isolation = BuildIsolation::default();
|
||||||
|
let hasher = HashStrategy::default();
|
||||||
|
let setup_py = SetupPyStrategy::default();
|
||||||
|
let flat_index = FlatIndex::default();
|
||||||
|
|
||||||
|
// Create a build dispatch.
|
||||||
|
let build_dispatch = BuildDispatch::new(
|
||||||
|
&client,
|
||||||
|
cache,
|
||||||
|
interpreter,
|
||||||
|
index_locations,
|
||||||
|
&flat_index,
|
||||||
|
&state.index,
|
||||||
|
&state.git,
|
||||||
|
&in_flight,
|
||||||
|
*index_strategy,
|
||||||
|
setup_py,
|
||||||
|
config_setting,
|
||||||
|
build_isolation,
|
||||||
|
*link_mode,
|
||||||
|
build_options,
|
||||||
|
*exclude_newer,
|
||||||
|
concurrency,
|
||||||
|
preview,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initialize the resolver.
|
||||||
|
let resolver = NamedRequirementsResolver::new(
|
||||||
|
requirements,
|
||||||
|
&hasher,
|
||||||
|
&state.index,
|
||||||
|
DistributionDatabase::new(&client, &build_dispatch, concurrency.downloads, preview),
|
||||||
|
)
|
||||||
|
.with_reporter(ResolverReporter::from(printer));
|
||||||
|
|
||||||
|
Ok(resolver.resolve().await?)
|
||||||
|
}
|
||||||
|
|
||||||
/// Update a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s.
|
/// Update a [`PythonEnvironment`] to satisfy a set of [`RequirementsSource`]s.
|
||||||
pub(crate) async fn update_environment(
|
pub(crate) async fn update_environment(
|
||||||
venv: PythonEnvironment,
|
venv: PythonEnvironment,
|
||||||
|
|
|
@ -28,12 +28,12 @@ use crate::settings::ResolverInstallerSettings;
|
||||||
|
|
||||||
/// Run a command.
|
/// Run a command.
|
||||||
pub(crate) async fn run(
|
pub(crate) async fn run(
|
||||||
extras: ExtrasSpecification,
|
|
||||||
dev: bool,
|
|
||||||
command: ExternalCommand,
|
command: ExternalCommand,
|
||||||
requirements: Vec<RequirementsSource>,
|
requirements: Vec<RequirementsSource>,
|
||||||
python: Option<String>,
|
|
||||||
package: Option<PackageName>,
|
package: Option<PackageName>,
|
||||||
|
extras: ExtrasSpecification,
|
||||||
|
dev: bool,
|
||||||
|
python: Option<String>,
|
||||||
settings: ResolverInstallerSettings,
|
settings: ResolverInstallerSettings,
|
||||||
isolated: bool,
|
isolated: bool,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
|
|
|
@ -4,11 +4,11 @@ use std::fmt::Write;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use anyhow::{bail, Context, Result};
|
use anyhow::{bail, Context, Result};
|
||||||
use distribution_types::Name;
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
use pypi_types::VerbatimParsedUrl;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
|
use distribution_types::Name;
|
||||||
|
use pypi_types::Requirement;
|
||||||
use uv_cache::Cache;
|
use uv_cache::Cache;
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
use uv_configuration::{Concurrency, PreviewMode, Reinstall};
|
use uv_configuration::{Concurrency, PreviewMode, Reinstall};
|
||||||
|
@ -16,13 +16,16 @@ use uv_configuration::{Concurrency, PreviewMode, Reinstall};
|
||||||
use uv_fs::replace_symlink;
|
use uv_fs::replace_symlink;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::SitePackages;
|
use uv_installer::SitePackages;
|
||||||
|
use uv_normalize::PackageName;
|
||||||
use uv_requirements::RequirementsSpecification;
|
use uv_requirements::RequirementsSpecification;
|
||||||
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
|
||||||
use uv_toolchain::{EnvironmentPreference, Toolchain, ToolchainPreference, ToolchainRequest};
|
use uv_toolchain::{
|
||||||
|
EnvironmentPreference, Interpreter, Toolchain, ToolchainPreference, ToolchainRequest,
|
||||||
|
};
|
||||||
use uv_warnings::warn_user_once;
|
use uv_warnings::warn_user_once;
|
||||||
|
|
||||||
use crate::commands::project::{update_environment, SharedState};
|
use crate::commands::project::{update_environment, SharedState};
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::{project, ExitStatus};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
use crate::settings::ResolverInstallerSettings;
|
use crate::settings::ResolverInstallerSettings;
|
||||||
|
|
||||||
|
@ -46,29 +49,71 @@ pub(crate) async fn install(
|
||||||
warn_user_once!("`uv tool install` is experimental and may change without warning.");
|
warn_user_once!("`uv tool install` is experimental and may change without warning.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let interpreter = Toolchain::find(
|
||||||
|
&python
|
||||||
|
.as_deref()
|
||||||
|
.map(ToolchainRequest::parse)
|
||||||
|
.unwrap_or_default(),
|
||||||
|
EnvironmentPreference::OnlySystem,
|
||||||
|
toolchain_preference,
|
||||||
|
cache,
|
||||||
|
)?
|
||||||
|
.into_interpreter();
|
||||||
|
|
||||||
|
// Initialize any shared state.
|
||||||
|
let state = SharedState::default();
|
||||||
|
|
||||||
|
// Resolve the `from` requirement.
|
||||||
let from = if let Some(from) = from {
|
let from = if let Some(from) = from {
|
||||||
let from_requirement = pep508_rs::Requirement::<VerbatimParsedUrl>::from_str(&from)?;
|
// Parse the positional name. If the user provided more than a package name, it's an error
|
||||||
// Check if the user provided more than just a name positionally or if that name conflicts with `--from`
|
// (e.g., `uv install foo==1.0 --from foo`).
|
||||||
if from_requirement.name.to_string() != package {
|
let Ok(package) = PackageName::from_str(&package) else {
|
||||||
// Determine if its an entirely different package or a conflicting specification
|
bail!("Package requirement `{from}` provided with `--from` conflicts with install request `{package}`")
|
||||||
let package_requirement =
|
};
|
||||||
pep508_rs::Requirement::<VerbatimParsedUrl>::from_str(&package)?;
|
|
||||||
if from_requirement.name == package_requirement.name {
|
let from_requirement = resolve_requirements(
|
||||||
bail!(
|
std::iter::once(from.as_str()),
|
||||||
"Package requirement `{}` provided with `--from` conflicts with install request `{}`",
|
&interpreter,
|
||||||
from,
|
&settings,
|
||||||
package
|
&state,
|
||||||
);
|
preview,
|
||||||
}
|
connectivity,
|
||||||
|
concurrency,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.pop()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Check if the positional name conflicts with `--from`.
|
||||||
|
if from_requirement.name != package {
|
||||||
|
// Determine if it's an entirely different package (e.g., `uv install foo --from bar`).
|
||||||
bail!(
|
bail!(
|
||||||
"Package name `{}` provided with `--from` does not match install request `{}`",
|
"Package name `{}` provided with `--from` does not match install request `{}`",
|
||||||
from_requirement.name,
|
from_requirement.name,
|
||||||
package
|
package
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
from_requirement
|
from_requirement
|
||||||
} else {
|
} else {
|
||||||
pep508_rs::Requirement::<VerbatimParsedUrl>::from_str(&package)?
|
resolve_requirements(
|
||||||
|
std::iter::once(package.as_str()),
|
||||||
|
&interpreter,
|
||||||
|
&settings,
|
||||||
|
&state,
|
||||||
|
preview,
|
||||||
|
connectivity,
|
||||||
|
concurrency,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.pop()
|
||||||
|
.unwrap()
|
||||||
};
|
};
|
||||||
|
|
||||||
let name = from.name.to_string();
|
let name = from.name.to_string();
|
||||||
|
@ -100,37 +145,28 @@ pub(crate) async fn install(
|
||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
let requirements = [Ok(from.clone())]
|
// Combine the `from` and `with` requirements.
|
||||||
.into_iter()
|
let requirements = {
|
||||||
.chain(
|
let mut requirements = Vec::with_capacity(1 + with.len());
|
||||||
with.iter()
|
requirements.push(from.clone());
|
||||||
.map(|name| pep508_rs::Requirement::from_str(name)),
|
requirements.extend(
|
||||||
)
|
resolve_requirements(
|
||||||
.collect::<Result<Vec<pep508_rs::Requirement<VerbatimParsedUrl>>, _>>()?;
|
with.iter().map(String::as_str),
|
||||||
|
&interpreter,
|
||||||
let spec = RequirementsSpecification::from_requirements(
|
&settings,
|
||||||
|
&state,
|
||||||
|
preview,
|
||||||
|
connectivity,
|
||||||
|
concurrency,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await?,
|
||||||
|
);
|
||||||
requirements
|
requirements
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(pypi_types::Requirement::from)
|
|
||||||
.collect(),
|
|
||||||
);
|
|
||||||
|
|
||||||
let Some(from) = requirements.first().cloned() else {
|
|
||||||
bail!("Expected at least one requirement")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let interpreter = Toolchain::find(
|
|
||||||
&python
|
|
||||||
.as_deref()
|
|
||||||
.map(ToolchainRequest::parse)
|
|
||||||
.unwrap_or_default(),
|
|
||||||
EnvironmentPreference::OnlySystem,
|
|
||||||
toolchain_preference,
|
|
||||||
cache,
|
|
||||||
)?
|
|
||||||
.into_interpreter();
|
|
||||||
|
|
||||||
// TODO(zanieb): Build the environment in the cache directory then copy into the tool directory
|
// TODO(zanieb): Build the environment in the cache directory then copy into the tool directory
|
||||||
// This lets us confirm the environment is valid before removing an existing install
|
// This lets us confirm the environment is valid before removing an existing install
|
||||||
let environment = installed_tools.environment(
|
let environment = installed_tools.environment(
|
||||||
|
@ -142,6 +178,7 @@ pub(crate) async fn install(
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
// Install the ephemeral requirements.
|
// Install the ephemeral requirements.
|
||||||
|
let spec = RequirementsSpecification::from_requirements(requirements.clone());
|
||||||
let environment = update_environment(
|
let environment = update_environment(
|
||||||
environment,
|
environment,
|
||||||
spec,
|
spec,
|
||||||
|
@ -260,7 +297,10 @@ pub(crate) async fn install(
|
||||||
debug!("Adding receipt for tool `{name}`");
|
debug!("Adding receipt for tool `{name}`");
|
||||||
let installed_tools = installed_tools.init()?;
|
let installed_tools = installed_tools.init()?;
|
||||||
let tool = Tool::new(
|
let tool = Tool::new(
|
||||||
requirements,
|
requirements
|
||||||
|
.into_iter()
|
||||||
|
.map(pep508_rs::Requirement::from)
|
||||||
|
.collect(),
|
||||||
python,
|
python,
|
||||||
target_entry_points
|
target_entry_points
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -270,3 +310,41 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve any [`UnnamedRequirements`].
|
||||||
|
pub(crate) async fn resolve_requirements(
|
||||||
|
requirements: impl Iterator<Item = &str>,
|
||||||
|
interpreter: &Interpreter,
|
||||||
|
settings: &ResolverInstallerSettings,
|
||||||
|
state: &SharedState,
|
||||||
|
preview: PreviewMode,
|
||||||
|
connectivity: Connectivity,
|
||||||
|
concurrency: Concurrency,
|
||||||
|
native_tls: bool,
|
||||||
|
cache: &Cache,
|
||||||
|
printer: Printer,
|
||||||
|
) -> Result<Vec<Requirement>> {
|
||||||
|
// Parse the requirements.
|
||||||
|
let requirements = {
|
||||||
|
let mut parsed = vec![];
|
||||||
|
for requirement in requirements {
|
||||||
|
parsed.push(RequirementsSpecification::parse_package(requirement)?);
|
||||||
|
}
|
||||||
|
parsed
|
||||||
|
};
|
||||||
|
|
||||||
|
// Resolve the parsed requirements.
|
||||||
|
project::resolve_names(
|
||||||
|
requirements,
|
||||||
|
interpreter,
|
||||||
|
settings,
|
||||||
|
state,
|
||||||
|
preview,
|
||||||
|
connectivity,
|
||||||
|
concurrency,
|
||||||
|
native_tls,
|
||||||
|
cache,
|
||||||
|
printer,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ use uv_cache::Cache;
|
||||||
use uv_cli::ExternalCommand;
|
use uv_cli::ExternalCommand;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::{Concurrency, PreviewMode};
|
use uv_configuration::{Concurrency, PreviewMode};
|
||||||
|
use uv_normalize::PackageName;
|
||||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||||
use uv_toolchain::{
|
use uv_toolchain::{
|
||||||
EnvironmentPreference, PythonEnvironment, Toolchain, ToolchainPreference, ToolchainRequest,
|
EnvironmentPreference, PythonEnvironment, Toolchain, ToolchainPreference, ToolchainRequest,
|
||||||
|
@ -27,9 +28,9 @@ use crate::settings::ResolverInstallerSettings;
|
||||||
/// Run a command.
|
/// Run a command.
|
||||||
pub(crate) async fn run(
|
pub(crate) async fn run(
|
||||||
command: ExternalCommand,
|
command: ExternalCommand,
|
||||||
python: Option<String>,
|
|
||||||
from: Option<String>,
|
from: Option<String>,
|
||||||
with: Vec<String>,
|
with: Vec<String>,
|
||||||
|
python: Option<String>,
|
||||||
settings: ResolverInstallerSettings,
|
settings: ResolverInstallerSettings,
|
||||||
_isolated: bool,
|
_isolated: bool,
|
||||||
preview: PreviewMode,
|
preview: PreviewMode,
|
||||||
|
@ -193,6 +194,12 @@ fn parse_target(target: &OsString) -> Result<(Cow<OsString>, Cow<str>)> {
|
||||||
return Ok((Cow::Borrowed(target), Cow::Borrowed(target_str)));
|
return Ok((Cow::Borrowed(target), Cow::Borrowed(target_str)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// e.g. ignore `git+https://github.com/uv/uv.git@main`
|
||||||
|
if PackageName::from_str(name).is_err() {
|
||||||
|
debug!("Ignoring non-package name `{}` in command", name);
|
||||||
|
return Ok((Cow::Borrowed(target), Cow::Borrowed(target_str)));
|
||||||
|
}
|
||||||
|
|
||||||
// e.g. `uv@0.1.0`, convert to `uv==0.1.0`
|
// e.g. `uv@0.1.0`, convert to `uv==0.1.0`
|
||||||
if let Ok(version) = Version::from_str(version) {
|
if let Ok(version) = Version::from_str(version) {
|
||||||
return Ok((
|
return Ok((
|
||||||
|
|
|
@ -649,12 +649,12 @@ async fn run() -> Result<ExitStatus> {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
commands::run(
|
commands::run(
|
||||||
args.extras,
|
|
||||||
args.dev,
|
|
||||||
args.command,
|
args.command,
|
||||||
requirements,
|
requirements,
|
||||||
args.python,
|
|
||||||
args.package,
|
args.package,
|
||||||
|
args.extras,
|
||||||
|
args.dev,
|
||||||
|
args.python,
|
||||||
args.settings,
|
args.settings,
|
||||||
globals.isolated,
|
globals.isolated,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
|
@ -789,9 +789,9 @@ async fn run() -> Result<ExitStatus> {
|
||||||
|
|
||||||
commands::tool_run(
|
commands::tool_run(
|
||||||
args.command,
|
args.command,
|
||||||
args.python,
|
|
||||||
args.from,
|
args.from,
|
||||||
args.with,
|
args.with,
|
||||||
|
args.python,
|
||||||
args.settings,
|
args.settings,
|
||||||
globals.isolated,
|
globals.isolated,
|
||||||
globals.preview,
|
globals.preview,
|
||||||
|
|
|
@ -883,3 +883,285 @@ fn tool_install_no_entrypoints() {
|
||||||
error: No entry points found for tool `iniconfig`
|
error: No entry points found for tool `iniconfig`
|
||||||
"###);
|
"###);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Test installing a tool with a bare URL requirement.
|
||||||
|
#[test]
|
||||||
|
fn tool_install_unnamed_package() {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
// Install `black`
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
Prepared 6 packages in [TIME]
|
||||||
|
Installed 6 packages in [TIME]
|
||||||
|
+ black==24.4.2 (from https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl)
|
||||||
|
+ click==8.1.7
|
||||||
|
+ mypy-extensions==1.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pathspec==0.12.1
|
||||||
|
+ platformdirs==4.2.0
|
||||||
|
Installed: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
tool_dir.child("black").assert(predicate::path::is_dir());
|
||||||
|
tool_dir
|
||||||
|
.child("black")
|
||||||
|
.child("uv-receipt.toml")
|
||||||
|
.assert(predicate::path::exists());
|
||||||
|
|
||||||
|
let executable = bin_dir.child(format!("black{}", std::env::consts::EXE_SUFFIX));
|
||||||
|
assert!(executable.exists());
|
||||||
|
|
||||||
|
// On Windows, we can't snapshot an executable file.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// Should run black in the virtual environment
|
||||||
|
assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###"
|
||||||
|
#![TEMP_DIR]/tools/black/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from black import patched_main
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||||
|
sys.exit(patched_main())
|
||||||
|
"###);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// We should have a tool receipt
|
||||||
|
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||||
|
[tool]
|
||||||
|
requirements = ["black @ https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), Command::new("black").arg("--version").env("PATH", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
black, 24.4.2 (compiled: no)
|
||||||
|
Python (CPython) 3.12.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test installing a tool with a bare URL requirement using `--from`, where the URL and the package
|
||||||
|
/// name conflict.
|
||||||
|
#[test]
|
||||||
|
fn tool_install_unnamed_conflict() {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
// Install `black`
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("black")
|
||||||
|
.arg("--from")
|
||||||
|
.arg("https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
error: Package name `iniconfig` provided with `--from` does not match install request `black`
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test installing a tool with a bare URL requirement using `--from`.
|
||||||
|
#[test]
|
||||||
|
fn tool_install_unnamed_from() {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
// Install `black`
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("black")
|
||||||
|
.arg("--from")
|
||||||
|
.arg("https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
Resolved 6 packages in [TIME]
|
||||||
|
Prepared 6 packages in [TIME]
|
||||||
|
Installed 6 packages in [TIME]
|
||||||
|
+ black==24.4.2 (from https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl)
|
||||||
|
+ click==8.1.7
|
||||||
|
+ mypy-extensions==1.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pathspec==0.12.1
|
||||||
|
+ platformdirs==4.2.0
|
||||||
|
Installed: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
tool_dir.child("black").assert(predicate::path::is_dir());
|
||||||
|
tool_dir
|
||||||
|
.child("black")
|
||||||
|
.child("uv-receipt.toml")
|
||||||
|
.assert(predicate::path::exists());
|
||||||
|
|
||||||
|
let executable = bin_dir.child(format!("black{}", std::env::consts::EXE_SUFFIX));
|
||||||
|
assert!(executable.exists());
|
||||||
|
|
||||||
|
// On Windows, we can't snapshot an executable file.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// Should run black in the virtual environment
|
||||||
|
assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###"
|
||||||
|
#![TEMP_DIR]/tools/black/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from black import patched_main
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||||
|
sys.exit(patched_main())
|
||||||
|
"###);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// We should have a tool receipt
|
||||||
|
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||||
|
[tool]
|
||||||
|
requirements = ["black @ https://files.pythonhosted.org/packages/0f/89/294c9a6b6c75a08da55e9d05321d0707e9418735e3062b12ef0f54c33474/black-24.4.2-py3-none-any.whl"]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), Command::new("black").arg("--version").env("PATH", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
black, 24.4.2 (compiled: no)
|
||||||
|
Python (CPython) 3.12.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Test installing a tool with a bare URL requirement using `--with`.
|
||||||
|
#[test]
|
||||||
|
fn tool_install_unnamed_with() {
|
||||||
|
let context = TestContext::new("3.12").with_filtered_exe_suffix();
|
||||||
|
let tool_dir = context.temp_dir.child("tools");
|
||||||
|
let bin_dir = context.temp_dir.child("bin");
|
||||||
|
|
||||||
|
// Install `black`
|
||||||
|
uv_snapshot!(context.filters(), context.tool_install()
|
||||||
|
.arg("black")
|
||||||
|
.arg("--with")
|
||||||
|
.arg("https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl")
|
||||||
|
.env("UV_TOOL_DIR", tool_dir.as_os_str())
|
||||||
|
.env("XDG_BIN_HOME", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
warning: `uv tool install` is experimental and may change without warning.
|
||||||
|
Resolved 7 packages in [TIME]
|
||||||
|
Prepared 7 packages in [TIME]
|
||||||
|
Installed 7 packages in [TIME]
|
||||||
|
+ black==24.3.0
|
||||||
|
+ click==8.1.7
|
||||||
|
+ iniconfig==2.0.0 (from https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl)
|
||||||
|
+ mypy-extensions==1.0.0
|
||||||
|
+ packaging==24.0
|
||||||
|
+ pathspec==0.12.1
|
||||||
|
+ platformdirs==4.2.0
|
||||||
|
Installed: black, blackd
|
||||||
|
"###);
|
||||||
|
|
||||||
|
tool_dir.child("black").assert(predicate::path::is_dir());
|
||||||
|
tool_dir
|
||||||
|
.child("black")
|
||||||
|
.child("uv-receipt.toml")
|
||||||
|
.assert(predicate::path::exists());
|
||||||
|
|
||||||
|
let executable = bin_dir.child(format!("black{}", std::env::consts::EXE_SUFFIX));
|
||||||
|
assert!(executable.exists());
|
||||||
|
|
||||||
|
// On Windows, we can't snapshot an executable file.
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// Should run black in the virtual environment
|
||||||
|
assert_snapshot!(fs_err::read_to_string(executable).unwrap(), @r###"
|
||||||
|
#![TEMP_DIR]/tools/black/bin/python
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from black import patched_main
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0])
|
||||||
|
sys.exit(patched_main())
|
||||||
|
"###);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
insta::with_settings!({
|
||||||
|
filters => context.filters(),
|
||||||
|
}, {
|
||||||
|
// We should have a tool receipt
|
||||||
|
assert_snapshot!(fs_err::read_to_string(tool_dir.join("black").join("uv-receipt.toml")).unwrap(), @r###"
|
||||||
|
[tool]
|
||||||
|
requirements = [
|
||||||
|
"black",
|
||||||
|
"iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl",
|
||||||
|
]
|
||||||
|
entrypoints = [
|
||||||
|
{ name = "black", install-path = "[TEMP_DIR]/bin/black" },
|
||||||
|
{ name = "blackd", install-path = "[TEMP_DIR]/bin/blackd" },
|
||||||
|
]
|
||||||
|
"###);
|
||||||
|
});
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), Command::new("black").arg("--version").env("PATH", bin_dir.as_os_str()), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
black, 24.3.0 (compiled: yes)
|
||||||
|
Python (CPython) 3.12.[X]
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue