Skip to content

Commit

Permalink
wip: Support installing additional executables in uv tool install
Browse files Browse the repository at this point in the history
  • Loading branch information
lucab committed Sep 20, 2024
1 parent 9164999 commit e40e3ff
Show file tree
Hide file tree
Showing 9 changed files with 203 additions and 34 deletions.
4 changes: 4 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3271,6 +3271,10 @@ pub struct ToolInstallArgs {
#[arg(long)]
pub with: Vec<String>,

/// Additionally include entrypoints from the following packages.
#[arg(long)]
pub i_want_ponies: Vec<String>,

/// Run all requirements listed in the given `requirements.txt` files.
#[arg(long, value_parser = parse_maybe_file_path)]
pub with_requirements: Vec<Maybe<PathBuf>>,
Expand Down
22 changes: 18 additions & 4 deletions crates/uv-tool/src/tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ impl TryFrom<ToolWire> for Tool {
pub struct ToolEntrypoint {
pub name: String,
pub install_path: PathBuf,
#[serde(skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
}

/// Format an array so that each element is on its own line and has a trailing comma.
Expand Down Expand Up @@ -118,10 +120,10 @@ impl Tool {
pub fn new(
requirements: Vec<Requirement>,
python: Option<String>,
entrypoints: impl Iterator<Item = ToolEntrypoint>,
entrypoints: Vec<ToolEntrypoint>,
options: ToolOptions,
) -> Self {
let mut entrypoints: Vec<_> = entrypoints.collect();
let mut entrypoints = entrypoints;
entrypoints.sort();
Self {
requirements,
Expand Down Expand Up @@ -204,12 +206,21 @@ impl Tool {
pub fn options(&self) -> &ToolOptions {
&self.options
}

/// Consume a given `Tool` returning its entrypoints.
pub fn into_entrypoints(self) -> Vec<ToolEntrypoint> {
self.entrypoints
}
}

impl ToolEntrypoint {
/// Create a new [`ToolEntrypoint`].
pub fn new(name: String, install_path: PathBuf) -> Self {
Self { name, install_path }
pub fn new(name: String, install_path: PathBuf, from: String) -> Self {
Self {
name,
install_path,
from: Some(from),
}
}

/// Returns the TOML table for this entrypoint.
Expand All @@ -221,6 +232,9 @@ impl ToolEntrypoint {
// Use cross-platform slashes so the toml string type does not change
value(PortablePath::from(&self.install_path).to_string()),
);
if let Some(from) = &self.from {
table.insert("from", value(from));
};
table
}
}
50 changes: 27 additions & 23 deletions crates/uv/src/commands/tool/common.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::fmt::Write;
use std::path::Path;
use std::{collections::BTreeSet, ffi::OsString};

use anyhow::{bail, Context};
Expand All @@ -16,10 +17,9 @@ use uv_installer::SitePackages;
use uv_python::PythonEnvironment;
use uv_settings::ToolOptions;
use uv_shell::Shell;
use uv_tool::{entrypoint_paths, find_executable_directory, InstalledTools, Tool, ToolEntrypoint};
use uv_tool::{entrypoint_paths, InstalledTools, Tool, ToolEntrypoint};
use uv_warnings::warn_user;

use crate::commands::ExitStatus;
use crate::printer::Printer;

/// Return all packages which contain an executable with the given name.
Expand Down Expand Up @@ -64,25 +64,23 @@ pub(crate) fn remove_entrypoints(tool: &Tool) {
/// Installs tool executables for a given package and handles any conflicts.
pub(crate) fn install_executables(
environment: &PythonEnvironment,
tool_name: &PackageName,
name: &PackageName,
installed_tools: &InstalledTools,
existing_entrypoints: &[ToolEntrypoint],
executable_directory: &Path,
options: ToolOptions,
force: bool,
python: Option<String>,
requirements: Vec<Requirement>,
printer: Printer,
) -> anyhow::Result<ExitStatus> {
) -> anyhow::Result<Tool> {
let site_packages = SitePackages::from_environment(environment)?;
let installed = site_packages.get_packages(name);
let Some(installed_dist) = installed.first().copied() else {
bail!("Expected at least one requirement")
};

// Find a suitable path to install into
let executable_directory = find_executable_directory()?;
fs_err::create_dir_all(&executable_directory)
.context("Failed to create executable directory")?;

debug!(
"Installing tool executables into: {}",
executable_directory.user_display()
Expand Down Expand Up @@ -120,7 +118,10 @@ pub(crate) fn install_executables(
// Clean up the environment we just created.
installed_tools.remove_environment(name)?;

return Ok(ExitStatus::Failure);
return Err(anyhow::anyhow!(
"Failed to install entrypoints for `{from}`",
from = name.cyan()
));
}

// Check if they exist, before installing
Expand All @@ -129,7 +130,7 @@ pub(crate) fn install_executables(
.filter(|(_, _, target_path)| target_path.exists())
.peekable();

// Ignore any existing entrypoints if the user passed `--force`, or the existing recept was
// Ignore any existing entrypoints if the user passed `--force`, or the existing receipt was
// broken.
if force {
for (name, _, target) in existing_entry_points {
Expand All @@ -138,7 +139,7 @@ pub(crate) fn install_executables(
}
} else if existing_entry_points.peek().is_some() {
// Clean up the environment we just created
installed_tools.remove_environment(name)?;
installed_tools.remove_environment(tool_name)?;

let existing_entry_points = existing_entry_points
// SAFETY: We know the target has a filename because we just constructed it above
Expand Down Expand Up @@ -182,20 +183,24 @@ pub(crate) fn install_executables(
)?;

debug!("Adding receipt for tool `{name}`");
let tool = Tool::new(
requirements.into_iter().collect(),
python,
target_entry_points
.into_iter()
.map(|(name, _, target_path)| ToolEntrypoint::new(name, target_path)),
options,
);
installed_tools.add_tool_receipt(name, tool)?;

let mut entrypoints = existing_entrypoints.to_vec();
for (entry, _, target_path) in target_entry_points {
entrypoints.push(ToolEntrypoint::new(entry, target_path, name.to_string()));
}
let tool = Tool::new(requirements, python, entrypoints, options);
installed_tools.add_tool_receipt(tool_name, tool.clone())?;

warn_out_of_path(executable_directory);

Ok(tool)
}

pub(crate) fn warn_out_of_path(executable_directory: &Path) {
// If the executable directory isn't on the user's PATH, warn.
if !Shell::contains_path(&executable_directory) {
if !Shell::contains_path(executable_directory) {
if let Some(shell) = Shell::from_env() {
if let Some(command) = shell.prepend_path(&executable_directory) {
if let Some(command) = shell.prepend_path(executable_directory) {
if shell.configuration_files().is_empty() {
warn_user!(
"`{}` is not on your PATH. To use installed tools, run `{}`.",
Expand Down Expand Up @@ -223,7 +228,6 @@ pub(crate) fn install_executables(
);
}
}
Ok(ExitStatus::Success)
}

/// Displays a hint if an executable matching the package name can be found in a dependency of the package.
Expand Down
54 changes: 49 additions & 5 deletions crates/uv/src/commands/tool/install.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
use std::fmt::Write;
use std::str::FromStr;

use anyhow::{bail, Result};
use anyhow::{bail, Context as _, Result};
use distribution_types::UnresolvedRequirementSpecification;
use owo_colors::OwoColorize;
use pep440_rs::{VersionSpecifier, VersionSpecifiers};
use pep508_rs::MarkerTree;
use pypi_types::{Requirement, RequirementSource};
use tracing::trace;
use tracing::{debug, trace};
use uv_cache::{Cache, Refresh};
use uv_cache_info::Timestamp;
use uv_client::{BaseClientBuilder, Connectivity};
Expand All @@ -26,7 +26,7 @@ use crate::commands::pip::loggers::{DefaultInstallLogger, DefaultResolveLogger};
use crate::commands::project::{
resolve_environment, resolve_names, sync_environment, update_environment,
};
use crate::commands::tool::common::remove_entrypoints;
use crate::commands::tool::common::{remove_entrypoints, warn_out_of_path};
use crate::commands::tool::Target;
use crate::commands::{reporters::PythonDownloadReporter, tool::common::install_executables};
use crate::commands::{ExitStatus, SharedState};
Expand All @@ -39,6 +39,7 @@ pub(crate) async fn install(
editable: bool,
from: Option<String>,
with: &[RequirementsSource],
extra_entrypoints_packages: &[String],
python: Option<String>,
force: bool,
options: ResolverInstallerOptions,
Expand Down Expand Up @@ -411,14 +412,57 @@ pub(crate) async fn install(
.await?
};

// TODO(lucab): we support performing multiple installs in the same environment.
// This force-installs, maybe it can be modeled in a more graceful way instead?
let force_install = force || invalid_tool_receipt || !extra_entrypoints_packages.is_empty();

// Find a suitable path to install into.
let executable_directory = uv_tool::find_executable_directory()?;
fs_err::create_dir_all(&executable_directory)
.context("Failed to create executable directory")?;

// Install additional entrypoints from dependencies,
// if any was explicitly requested.
let mut deps_entrypoints = vec![];
for entry in extra_entrypoints_packages {
let pkg = PackageName::from_str(entry)?;
debug!(
"Installing entrypoints for {} as part of tool {}",
pkg, from.name
);
let tool = install_executables(
&environment,
&from.name,
&pkg,
&installed_tools,
&deps_entrypoints,
&executable_directory,
options.clone(),
force_install,
python.clone(),
requirements.clone(),
printer,
)?;
deps_entrypoints.append(&mut tool.into_entrypoints());
}

// Install entrypoints from the target package.
debug!("Installing entrypoints tool {}", from.name);
install_executables(
&environment,
&from.name,
&from.name,
&installed_tools,
&deps_entrypoints,
&executable_directory,
options,
force || invalid_tool_receipt,
force_install,
python,
requirements,
printer,
)
)?;

warn_out_of_path(&executable_directory);

Ok(ExitStatus::Success)
}
14 changes: 13 additions & 1 deletion crates/uv/src/commands/tool/upgrade.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{collections::BTreeSet, fmt::Write};

use anyhow::Result;
use anyhow::{Context as _, Result};
use owo_colors::OwoColorize;
use tracing::debug;

Expand All @@ -20,6 +20,8 @@ use crate::commands::{tool::common::install_executables, ExitStatus, SharedState
use crate::printer::Printer;
use crate::settings::ResolverInstallerSettings;

use super::common::warn_out_of_path;

/// Upgrade a tool.
pub(crate) async fn upgrade(
name: Vec<PackageName>,
Expand Down Expand Up @@ -196,16 +198,26 @@ async fn upgrade_tool(
// existing executables.
remove_entrypoints(&existing_tool_receipt);

// Find a suitable path to install into.
let executable_directory = uv_tool::find_executable_directory()?;
fs_err::create_dir_all(&executable_directory)
.context("Failed to create executable directory")?;

install_executables(
&environment,
name,
name,
installed_tools,
&[],
&executable_directory,
ToolOptions::from(options),
true,
existing_tool_receipt.python().to_owned(),
requirements.to_vec(),
printer,
)?;

warn_out_of_path(&executable_directory);
}

Ok(changelog)
Expand Down
9 changes: 8 additions & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -877,8 +877,14 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
.combine(Refresh::from(args.settings.upgrade.clone())),
);

let requirements = args
// Synthesize extra dependencies by merging all additionally
// specified requirements.
let extra_deps = args
.with
.into_iter()
.chain(args.extra_entrypoints_packages.iter().cloned());

let requirements = extra_deps
.into_iter()
.map(RequirementsSource::from_package)
.chain(
Expand All @@ -893,6 +899,7 @@ async fn run(cli: Cli) -> Result<ExitStatus> {
args.editable,
args.from,
&requirements,
&args.extra_entrypoints_packages,
args.python,
args.force,
args.options,
Expand Down
3 changes: 3 additions & 0 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,7 @@ pub(crate) struct ToolInstallSettings {
pub(crate) from: Option<String>,
pub(crate) with: Vec<String>,
pub(crate) with_requirements: Vec<PathBuf>,
pub(crate) extra_entrypoints_packages: Vec<String>,
pub(crate) python: Option<String>,
pub(crate) refresh: Refresh,
pub(crate) options: ResolverInstallerOptions,
Expand All @@ -387,6 +388,7 @@ impl ToolInstallSettings {
from,
with,
with_requirements,
i_want_ponies,
installer,
force,
build,
Expand All @@ -411,6 +413,7 @@ impl ToolInstallSettings {
.into_iter()
.filter_map(Maybe::into_option)
.collect(),
extra_entrypoints_packages: i_want_ponies,
python,
force,
editable,
Expand Down
Loading

0 comments on commit e40e3ff

Please sign in to comment.