mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
Add uv tool install
(#4492)
This is the minimal "working" implementation. In summary, we: - Resolve the requested requirements - Create an environment at `$UV_STATE_DIR/tools/$name` - Inspect the `dist-info` for the main requirement to determine its entry points scripts - Link the entry points from a user-executable directory (`$XDG_BIN_HOME`) to the environment bin - Create an entry at `$UV_STATE_DIR/tools/tools.toml` tracking the user's request The idea with `tools.toml` is that it allows us to perform upgrades and syncs, retaining the original user request (similar to declarations in a `pyproject.toml`). I imagine using a similar schema in the `pyproject.toml` in the future if/when we add project-levle tools. I'm also considering exposing `tools.toml` in the standard uv configuration directory instead of the state directory, but it seems nice to tuck it away for now while we iterate on it. Installing a tool won't perform a sync of other tool environments, we'll probably have an explicit `uv tool sync` command for that? I've split out todos into follow-up pull requests: - #4509 (failing on Windows) - #4501 - #4504 Closes #4485
This commit is contained in:
parent
b677a06aba
commit
c9657b0015
18 changed files with 744 additions and 26 deletions
|
@ -11,9 +11,11 @@ use zip::result::ZipError;
|
|||
use pep440_rs::Version;
|
||||
use platform_tags::{Arch, Os};
|
||||
use pypi_types::Scheme;
|
||||
pub use script::{scripts_from_ini, Script};
|
||||
pub use uninstall::{uninstall_egg, uninstall_legacy_editable, uninstall_wheel, Uninstall};
|
||||
use uv_fs::Simplified;
|
||||
use uv_normalize::PackageName;
|
||||
pub use wheel::{parse_wheel_file, LibKind};
|
||||
|
||||
pub mod linker;
|
||||
pub mod metadata;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Like `wheel.rs`, but for installing wheels that have already been unzipped, rather than
|
||||
//! reading from a zip file.
|
||||
|
||||
use std::path::Path;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use std::time::SystemTime;
|
||||
|
||||
|
@ -143,6 +143,24 @@ pub fn install_wheel(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Determine the absolute path to an entrypoint script.
|
||||
pub fn entrypoint_path(entrypoint: &Script, layout: &Layout) -> PathBuf {
|
||||
if cfg!(windows) {
|
||||
// On windows we actually build an .exe wrapper
|
||||
let script_name = entrypoint
|
||||
.name
|
||||
// FIXME: What are the in-reality rules here for names?
|
||||
.strip_suffix(".py")
|
||||
.unwrap_or(&entrypoint.name)
|
||||
.to_string()
|
||||
+ ".exe";
|
||||
|
||||
layout.scheme.scripts.join(script_name)
|
||||
} else {
|
||||
layout.scheme.scripts.join(&entrypoint.name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the `dist-info` directory in an unzipped wheel.
|
||||
///
|
||||
/// See: <https://github.com/PyO3/python-pkginfo-rs>
|
||||
|
|
|
@ -9,10 +9,10 @@ use crate::{wheel, Error};
|
|||
/// A script defining the name of the runnable entrypoint and the module and function that should be
|
||||
/// run.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
pub(crate) struct Script {
|
||||
pub(crate) name: String,
|
||||
pub(crate) module: String,
|
||||
pub(crate) function: String,
|
||||
pub struct Script {
|
||||
pub name: String,
|
||||
pub module: String,
|
||||
pub function: String,
|
||||
}
|
||||
|
||||
impl Script {
|
||||
|
@ -64,7 +64,7 @@ impl Script {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scripts_from_ini(
|
||||
pub fn scripts_from_ini(
|
||||
extras: Option<&[String]>,
|
||||
python_minor: u8,
|
||||
ini: String,
|
||||
|
|
|
@ -17,6 +17,7 @@ use zip::ZipWriter;
|
|||
use pypi_types::DirectUrl;
|
||||
use uv_fs::{relative_to, Simplified};
|
||||
|
||||
use crate::linker::entrypoint_path;
|
||||
use crate::record::RecordEntry;
|
||||
use crate::script::Script;
|
||||
use crate::{Error, Layout};
|
||||
|
@ -255,20 +256,7 @@ pub(crate) fn write_script_entrypoints(
|
|||
is_gui: bool,
|
||||
) -> Result<(), Error> {
|
||||
for entrypoint in entrypoints {
|
||||
let entrypoint_absolute = if cfg!(windows) {
|
||||
// On windows we actually build an .exe wrapper
|
||||
let script_name = entrypoint
|
||||
.name
|
||||
// FIXME: What are the in-reality rules here for names?
|
||||
.strip_suffix(".py")
|
||||
.unwrap_or(&entrypoint.name)
|
||||
.to_string()
|
||||
+ ".exe";
|
||||
|
||||
layout.scheme.scripts.join(script_name)
|
||||
} else {
|
||||
layout.scheme.scripts.join(&entrypoint.name)
|
||||
};
|
||||
let entrypoint_absolute = entrypoint_path(entrypoint, layout);
|
||||
|
||||
let entrypoint_relative = pathdiff::diff_paths(&entrypoint_absolute, site_packages)
|
||||
.ok_or_else(|| {
|
||||
|
@ -320,7 +308,7 @@ pub(crate) fn write_script_entrypoints(
|
|||
|
||||
/// Whether the wheel should be installed into the `purelib` or `platlib` directory.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(crate) enum LibKind {
|
||||
pub enum LibKind {
|
||||
/// Install into the `purelib` directory.
|
||||
Pure,
|
||||
/// Install into the `platlib` directory.
|
||||
|
@ -331,7 +319,7 @@ pub(crate) enum LibKind {
|
|||
///
|
||||
/// > {distribution}-{version}.dist-info/WHEEL is metadata about the archive itself in the same
|
||||
/// > basic key: value format:
|
||||
pub(crate) fn parse_wheel_file(wheel_text: &str) -> Result<LibKind, Error> {
|
||||
pub fn parse_wheel_file(wheel_text: &str) -> Result<LibKind, Error> {
|
||||
// {distribution}-{version}.dist-info/WHEEL is metadata about the archive itself in the same basic key: value format:
|
||||
let data = parse_key_value_file(&mut wheel_text.as_bytes(), "WHEEL")?;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue