mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 21:35:00 +00:00
Generate versioned pip launchers (#1918)
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
(3898741e29/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
This commit is contained in:
parent
daa8565a75
commit
11ed4f7183
4 changed files with 100 additions and 46 deletions
|
@ -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;
|
||||
|
@ -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};
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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()
|
||||
|
@ -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)]
|
||||
|
|
|
@ -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.
|
||||
|
@ -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;
|
||||
|
@ -92,6 +130,7 @@ mod test {
|
|||
assert!(Script::from_value("script", case, None).is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_script_names() {
|
||||
for case in [
|
||||
|
|
|
@ -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};
|
||||
|
@ -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`
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue