Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect data scripts in uv tool install #4693

Merged
merged 1 commit into from
Jul 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions crates/install-wheel-rs/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@ 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 use wheel::{parse_wheel_file, read_record_file, LibKind};

pub mod linker;
pub mod metadata;
Expand Down
20 changes: 1 addition & 19 deletions crates/install-wheel-rs/src/linker.rs
Original file line number Diff line number Diff line change
@@ -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, PathBuf};
use std::path::Path;
use std::str::FromStr;
use std::time::SystemTime;

Expand Down Expand Up @@ -143,24 +143,6 @@ 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>
Expand Down
8 changes: 4 additions & 4 deletions crates/install-wheel-rs/src/record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ use serde::{Deserialize, Serialize};
/// tqdm-4.62.3.dist-info/RECORD,,
/// ```
#[derive(Deserialize, Serialize, PartialOrd, PartialEq, Ord, Eq)]
pub(crate) struct RecordEntry {
pub(crate) path: String,
pub(crate) hash: Option<String>,
pub struct RecordEntry {
pub path: String,
pub hash: Option<String>,
#[allow(dead_code)]
pub(crate) size: Option<u64>,
pub size: Option<u64>,
}
10 changes: 5 additions & 5 deletions crates/install-wheel-rs/src/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 struct Script {
pub name: String,
pub module: String,
pub function: String,
pub(crate) struct Script {
pub(crate) name: String,
pub(crate) module: String,
pub(crate) function: String,
}

impl Script {
Expand Down Expand Up @@ -64,7 +64,7 @@ impl Script {
}
}

pub fn scripts_from_ini(
pub(crate) fn scripts_from_ini(
extras: Option<&[String]>,
python_minor: u8,
ini: String,
Expand Down
21 changes: 19 additions & 2 deletions crates/install-wheel-rs/src/wheel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ 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};
Expand Down Expand Up @@ -247,6 +246,24 @@ fn get_script_executable(python_executable: &Path, is_gui: bool) -> PathBuf {
}
}

/// Determine the absolute path to an entrypoint script.
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)
}
}

/// Create the wrapper scripts in the bin folder of the venv for launching console scripts.
pub(crate) fn write_script_entrypoints(
layout: &Layout,
Expand Down Expand Up @@ -632,7 +649,7 @@ pub(crate) fn extra_dist_info(

/// Reads the record file
/// <https://www.python.org/dev/peps/pep-0376/#record>
pub(crate) fn read_record_file(record: &mut impl Read) -> Result<Vec<RecordEntry>, Error> {
pub fn read_record_file(record: &mut impl Read) -> Result<Vec<RecordEntry>, Error> {
csv::ReaderBuilder::new()
.has_headers(false)
.escape(Some(b'"'))
Expand Down
5 changes: 3 additions & 2 deletions crates/uv-tool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ uv-warnings = { workspace = true }
dirs-sys = { workspace = true }
fs-err = { workspace = true }
path-slash = { workspace = true }
pathdiff = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
toml = { workspace = true }
toml_edit = { workspace = true }
toml = { workspace = true }
toml_edit = { workspace = true }
tracing = { workspace = true }
95 changes: 52 additions & 43 deletions crates/uv-tool/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
use core::fmt;
use fs_err as fs;
use install_wheel_rs::linker::entrypoint_path;
use install_wheel_rs::{scripts_from_ini, Script};
use pep440_rs::Version;
use pep508_rs::PackageName;
use std::io::{self, Write};
use std::path::{Path, PathBuf};

use fs_err as fs;
use fs_err::File;
use thiserror::Error;
use tracing::debug;

use install_wheel_rs::read_record_file;
use pep440_rs::Version;
use pep508_rs::PackageName;
pub use receipt::ToolReceipt;
pub use tool::{Tool, ToolEntrypoint};
use uv_cache::Cache;
use uv_fs::{LockedFile, Simplified};
use uv_state::{StateBucket, StateStore};
use uv_toolchain::{Interpreter, PythonEnvironment};
use uv_warnings::warn_user_once;

pub use receipt::ToolReceipt;
pub use tool::{Tool, ToolEntrypoint};

use uv_state::{StateBucket, StateStore};
mod receipt;
mod tool;

Expand Down Expand Up @@ -291,7 +292,7 @@ pub fn find_executable_directory() -> Result<PathBuf, Error> {
.ok_or(Error::NoExecutableDirectory)
}

/// Find the dist-info directory for a package in an environment.
/// Find the `.dist-info` directory for a package in an environment.
fn find_dist_info(
environment: &PythonEnvironment,
package_name: &PackageName,
Expand All @@ -306,53 +307,61 @@ fn find_dist_info(
.interpreter()
.site_packages()
.map(|path| path.join(&dist_info_prefix))
.find(|path| path.exists())
.find(|path| path.is_dir())
.ok_or_else(|| Error::DistInfoMissing(dist_info_prefix, environment.root().to_path_buf()))
}

/// Parses the `entry_points.txt` entry for console scripts
///
/// Returns (`script_name`, module, function)
fn parse_scripts(
dist_info_path: &Path,
python_minor: u8,
) -> Result<(Vec<Script>, Vec<Script>), Error> {
let entry_points_path = dist_info_path.join("entry_points.txt");

// Read the entry points mapping. If the file doesn't exist, we just return an empty mapping.
let Ok(ini) = fs::read_to_string(&entry_points_path) else {
debug!(
"Failed to read entry points at {}",
entry_points_path.user_display()
);
return Ok((Vec::new(), Vec::new()));
};

Ok(scripts_from_ini(None, python_minor, ini)?)
}

/// Find the paths to the entry points provided by a package in an environment.
///
/// Entry points can either be true Python entrypoints (defined in `entrypoints.txt`) or scripts in
/// the `.data` directory.
///
/// Returns a list of `(name, path)` tuples.
pub fn entrypoint_paths(
environment: &PythonEnvironment,
package_name: &PackageName,
package_version: &Version,
) -> Result<Vec<(String, PathBuf)>, Error> {
// Find the `.dist-info` directory in the installed environment.
let dist_info_path = find_dist_info(environment, package_name, package_version)?;
debug!("Looking at dist-info at {}", dist_info_path.user_display());
debug!(
"Looking at `.dist-info` at: {}",
dist_info_path.user_display()
);

let (console_scripts, gui_scripts) =
parse_scripts(&dist_info_path, environment.interpreter().python_minor())?;
// Read the RECORD file.
let record = read_record_file(&mut File::open(dist_info_path.join("RECORD"))?)?;

// The RECORD file uses relative paths, so we're looking for the relative path to be a prefix.
let layout = environment.interpreter().layout();
let script_relative = pathdiff::diff_paths(&layout.scheme.scripts, &layout.scheme.purelib)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::Other,
format!(
"Could not find relative path for: {}",
layout.scheme.scripts.simplified_display()
),
)
})?;

// Identify any installed binaries (both entrypoints and scripts from the `.data` directory).
let mut entrypoints = vec![];
for entry in record {
let relative_path = PathBuf::from(&entry.path);
let Ok(path_in_scripts) = relative_path.strip_prefix(&script_relative) else {
continue;
};

let absolute_path = layout.scheme.scripts.join(path_in_scripts);
let script_name = entry
.path
.rsplit(std::path::MAIN_SEPARATOR)
.next()
.unwrap_or(&entry.path)
.to_string();
entrypoints.push((script_name, absolute_path));
}

Ok(console_scripts
.into_iter()
.chain(gui_scripts)
.map(|entrypoint| {
let path = entrypoint_path(&entrypoint, &layout);
(entrypoint.name, path)
})
.collect())
Ok(entrypoints)
}
6 changes: 3 additions & 3 deletions crates/uv/tests/tool_uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ mod common;

#[test]
fn tool_uninstall() {
let context = TestContext::new("3.12");
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");

Expand Down Expand Up @@ -71,7 +71,7 @@ fn tool_uninstall() {

#[test]
fn tool_uninstall_not_installed() {
let context = TestContext::new("3.12");
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");

Expand All @@ -90,7 +90,7 @@ fn tool_uninstall_not_installed() {

#[test]
fn tool_uninstall_missing_receipt() {
let context = TestContext::new("3.12");
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");

Expand Down
Loading