Skip to content

Commit

Permalink
Special case pip entrypoint names
Browse files Browse the repository at this point in the history
Users expect pip to have `pip`, `pip3` and `pip3.x` entrypoints. But pip is a universal wheel, so it contains the `pip3.x` entrypoint where it was built on. To fix this, pip special cases itself when installing (https://github.com/pypa/pip/blob/3898741e29b7279e7bffe044ecfbe20f6a438b1e/src/pip/_internal/operations/install/wheel.py#L283), replacing the wheel entrypoint with one for the current version. We now do the same.

Fixes #1593
  • Loading branch information
konstin committed Feb 23, 2024
1 parent fe18475 commit 0153403
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 46 deletions.
29 changes: 7 additions & 22 deletions crates/install-wheel-rs/src/linker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
use std::path::Path;
use std::str::FromStr;

use configparser::ini::Ini;
use fs_err as fs;
use fs_err::{DirEntry, File};
use reflink_copy as reflink;
Expand All @@ -17,9 +16,9 @@ use pypi_types::DirectUrl;
use uv_normalize::PackageName;

use crate::install_location::InstallLocation;
use crate::script::scripts_from_ini;
use crate::wheel::{
extra_dist_info, install_data, parse_metadata, parse_wheel_version, read_scripts_from_section,
write_script_entrypoints,
extra_dist_info, install_data, parse_metadata, parse_wheel_version, write_script_entrypoints,
};
use crate::{read_record_file, Error, Script};

Expand Down Expand Up @@ -99,7 +98,8 @@ pub fn install_wheel(
let mut record = read_record_file(&mut record_file)?;

debug!(name, "Writing entrypoints");
let (console_scripts, gui_scripts) = parse_scripts(&wheel, &dist_info_prefix, None)?;
let (console_scripts, gui_scripts) =
parse_scripts(&wheel, &dist_info_prefix, None, location.python_version().1)?;
write_script_entrypoints(
&site_packages,
location,
Expand Down Expand Up @@ -200,11 +200,12 @@ fn dist_info_metadata(dist_info_prefix: &str, wheel: impl AsRef<Path>) -> Result
///
/// Returns (`script_name`, module, function)
///
/// Extras are supposed to be ignored, which happens if you pass None for extras
/// Extras are supposed to be ignored, which happens if you pass None for extras.
fn parse_scripts(
wheel: impl AsRef<Path>,
dist_info_prefix: &str,
extras: Option<&[String]>,
python_minor: u8,
) -> Result<(Vec<Script>, Vec<Script>), Error> {
let entry_points_path = wheel
.as_ref()
Expand All @@ -215,23 +216,7 @@ fn parse_scripts(
return Ok((Vec::new(), Vec::new()));
};

let entry_points_mapping = Ini::new_cs()
.read(ini)
.map_err(|err| Error::InvalidWheel(format!("entry_points.txt is invalid: {err}")))?;

// TODO: handle extras
let console_scripts = match entry_points_mapping.get("console_scripts") {
Some(console_scripts) => {
read_scripts_from_section(console_scripts, "console_scripts", extras)?
}
None => Vec::new(),
};
let gui_scripts = match entry_points_mapping.get("gui_scripts") {
Some(gui_scripts) => read_scripts_from_section(gui_scripts, "gui_scripts", extras)?,
None => Vec::new(),
};

Ok((console_scripts, gui_scripts))
scripts_from_ini(extras, python_minor, ini)
}

#[derive(Debug, Clone, Copy)]
Expand Down
41 changes: 40 additions & 1 deletion crates/install-wheel-rs/src/script.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use configparser::ini::Ini;
use once_cell::sync::Lazy;
use regex::Regex;
use rustc_hash::FxHashSet;
use serde::Serialize;

use crate::Error;
use crate::{wheel, Error};

/// A script defining the name of the runnable entrypoint and the module and function that should be
/// run.
Expand Down Expand Up @@ -78,6 +79,43 @@ impl Script {
}
}

pub(crate) fn scripts_from_ini(
extras: Option<&[String]>,
python_minor: u8,
ini: String,
) -> Result<(Vec<Script>, Vec<Script>), Error> {
let entry_points_mapping = Ini::new_cs()
.read(ini)
.map_err(|err| Error::InvalidWheel(format!("entry_points.txt is invalid: {err}")))?;

// TODO: handle extras
let mut console_scripts = match entry_points_mapping.get("console_scripts") {
Some(console_scripts) => {
wheel::read_scripts_from_section(console_scripts, "console_scripts", extras)?
}
None => Vec::new(),
};
let gui_scripts = match entry_points_mapping.get("gui_scripts") {
Some(gui_scripts) => wheel::read_scripts_from_section(gui_scripts, "gui_scripts", extras)?,
None => Vec::new(),
};

// Special case to generate versioned pip launchers.
// https://github.com/pypa/pip/blob/3898741e29b7279e7bffe044ecfbe20f6a438b1e/src/pip/_internal/operations/install/wheel.py#L283
// https://github.com/astral-sh/uv/issues/1593
for script in &mut console_scripts {
let Some((left, right)) = script.script_name.split_once('.') else {
continue;
};
if left != "pip3" || right.parse::<u8>().is_err() {
continue;
}
script.script_name = format!("pip3.{python_minor}");
}

Ok((console_scripts, gui_scripts))
}

#[cfg(test)]
mod test {
use crate::Script;
Expand All @@ -92,6 +130,7 @@ mod test {
assert!(Script::from_value("script", case, None).is_ok());
}
}

#[test]
fn test_invalid_script_names() {
for case in [
Expand Down
34 changes: 11 additions & 23 deletions crates/install-wheel-rs/src/wheel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ use std::process::{Command, ExitStatus, Stdio};
use std::str::FromStr;
use std::{env, io, iter};

use configparser::ini::Ini;
use data_encoding::BASE64URL_NOPAD;
use fs_err as fs;
use fs_err::{DirEntry, File};
Expand All @@ -27,7 +26,7 @@ use uv_normalize::PackageName;

use crate::install_location::{InstallLocation, LockedDir};
use crate::record::RecordEntry;
use crate::script::Script;
use crate::script::{scripts_from_ini, Script};
use crate::{find_dist_info, Error};

/// `#!/usr/bin/env python`
Expand Down Expand Up @@ -107,32 +106,16 @@ fn parse_scripts<R: Read + Seek>(
archive: &mut ZipArchive<R>,
dist_info_dir: &str,
extras: Option<&[String]>,
python_minor: u8,
) -> Result<(Vec<Script>, Vec<Script>), Error> {
let entry_points_path = format!("{dist_info_dir}/entry_points.txt");
let entry_points_mapping = match archive.by_name(&entry_points_path) {
Ok(file) => {
let ini_text = std::io::read_to_string(file)?;
Ini::new_cs()
.read(ini_text)
.map_err(|err| Error::InvalidWheel(format!("entry_points.txt is invalid: {err}")))?
}
let ini = match archive.by_name(&entry_points_path) {
Ok(file) => std::io::read_to_string(file)?,
Err(ZipError::FileNotFound) => return Ok((Vec::new(), Vec::new())),
Err(err) => return Err(Error::Zip(entry_points_path, err)),
};

// TODO: handle extras
let console_scripts = match entry_points_mapping.get("console_scripts") {
Some(console_scripts) => {
read_scripts_from_section(console_scripts, "console_scripts", extras)?
}
None => Vec::new(),
};
let gui_scripts = match entry_points_mapping.get("gui_scripts") {
Some(gui_scripts) => read_scripts_from_section(gui_scripts, "gui_scripts", extras)?,
None => Vec::new(),
};

Ok((console_scripts, gui_scripts))
scripts_from_ini(extras, python_minor, ini)
}

/// Shamelessly stolen (and updated for recent sha2)
Expand Down Expand Up @@ -1045,7 +1028,12 @@ pub fn install_wheel(
);

debug!(name = name.as_str(), "Writing entrypoints");
let (console_scripts, gui_scripts) = parse_scripts(&mut archive, &dist_info_prefix, None)?;
let (console_scripts, gui_scripts) = parse_scripts(
&mut archive,
&dist_info_prefix,
None,
location.python_version().1,
)?;
write_script_entrypoints(
&site_packages,
location,
Expand Down
42 changes: 42 additions & 0 deletions crates/uv/tests/pip_sync.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#![cfg(all(feature = "python", feature = "pypi"))]

use std::env::consts::EXE_SUFFIX;
use std::fs;
use std::path::Path;
use std::process::Command;
Expand Down Expand Up @@ -2757,3 +2758,44 @@ fn set_read_permissions() -> Result<()> {

Ok(())
}

/// Test special case to generate versioned pip launchers.
/// https://github.com/pypa/pip/blob/3898741e29b7279e7bffe044ecfbe20f6a438b1e/src/pip/_internal/operations/install/wheel.py#L283
/// https://github.com/astral-sh/uv/issues/1593
#[test]
fn pip_entrypoints() -> Result<()> {
let context = TestContext::new("3.12");

let requirements_txt = context.temp_dir.child("requirements.txt");
requirements_txt.touch()?;
requirements_txt.write_str("pip==24.0")?;

uv_snapshot!(command(&context)
.arg("requirements.txt")
.arg("--strict"), @r###"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 1 package in [TIME]
Downloaded 1 package in [TIME]
Installed 1 package in [TIME]
+ pip==24.0
"###
);

let bin_dir = context.venv.join(if cfg!(unix) {
"bin"
} else if cfg!(windows) {
"Scripts"
} else {
unimplemented!("Only Windows and Unix are supported")
});
// Pip 24.0 contains a pip3.10 launcher.
// https://inspector.pypi.io/project/pip/24.0/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl/pip-24.0.dist-info/entry_points.txt
assert!(!bin_dir.join(format!("pip3.10{}", EXE_SUFFIX)).exists());
assert!(bin_dir.join(format!("pip3.12{}", EXE_SUFFIX)).exists());

Ok(())
}

0 comments on commit 0153403

Please sign in to comment.