mirror of
https://github.com/astral-sh/uv.git
synced 2025-10-04 15:54:44 +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::path::Path;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use configparser::ini::Ini;
|
|
||||||
use fs_err as fs;
|
use fs_err as fs;
|
||||||
use fs_err::{DirEntry, File};
|
use fs_err::{DirEntry, File};
|
||||||
use reflink_copy as reflink;
|
use reflink_copy as reflink;
|
||||||
|
@ -17,9 +16,9 @@ use pypi_types::DirectUrl;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::install_location::InstallLocation;
|
use crate::install_location::InstallLocation;
|
||||||
|
use crate::script::scripts_from_ini;
|
||||||
use crate::wheel::{
|
use crate::wheel::{
|
||||||
extra_dist_info, install_data, parse_metadata, parse_wheel_version, read_scripts_from_section,
|
extra_dist_info, install_data, parse_metadata, parse_wheel_version, write_script_entrypoints,
|
||||||
write_script_entrypoints,
|
|
||||||
};
|
};
|
||||||
use crate::{read_record_file, Error, Script};
|
use crate::{read_record_file, Error, Script};
|
||||||
|
|
||||||
|
@ -99,7 +98,8 @@ pub fn install_wheel(
|
||||||
let mut record = read_record_file(&mut record_file)?;
|
let mut record = read_record_file(&mut record_file)?;
|
||||||
|
|
||||||
debug!(name, "Writing entrypoints");
|
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(
|
write_script_entrypoints(
|
||||||
&site_packages,
|
&site_packages,
|
||||||
location,
|
location,
|
||||||
|
@ -200,11 +200,12 @@ fn dist_info_metadata(dist_info_prefix: &str, wheel: impl AsRef<Path>) -> Result
|
||||||
///
|
///
|
||||||
/// Returns (`script_name`, module, function)
|
/// 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(
|
fn parse_scripts(
|
||||||
wheel: impl AsRef<Path>,
|
wheel: impl AsRef<Path>,
|
||||||
dist_info_prefix: &str,
|
dist_info_prefix: &str,
|
||||||
extras: Option<&[String]>,
|
extras: Option<&[String]>,
|
||||||
|
python_minor: u8,
|
||||||
) -> Result<(Vec<Script>, Vec<Script>), Error> {
|
) -> Result<(Vec<Script>, Vec<Script>), Error> {
|
||||||
let entry_points_path = wheel
|
let entry_points_path = wheel
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -215,23 +216,7 @@ fn parse_scripts(
|
||||||
return Ok((Vec::new(), Vec::new()));
|
return Ok((Vec::new(), Vec::new()));
|
||||||
};
|
};
|
||||||
|
|
||||||
let entry_points_mapping = Ini::new_cs()
|
scripts_from_ini(extras, python_minor, ini)
|
||||||
.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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
|
use configparser::ini::Ini;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use serde::Serialize;
|
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
|
/// A script defining the name of the runnable entrypoint and the module and function that should be
|
||||||
/// run.
|
/// 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)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use crate::Script;
|
use crate::Script;
|
||||||
|
@ -92,6 +130,7 @@ mod test {
|
||||||
assert!(Script::from_value("script", case, None).is_ok());
|
assert!(Script::from_value("script", case, None).is_ok());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_script_names() {
|
fn test_invalid_script_names() {
|
||||||
for case in [
|
for case in [
|
||||||
|
|
|
@ -5,7 +5,6 @@ use std::process::{Command, ExitStatus, Stdio};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::{env, io, iter};
|
use std::{env, io, iter};
|
||||||
|
|
||||||
use configparser::ini::Ini;
|
|
||||||
use data_encoding::BASE64URL_NOPAD;
|
use data_encoding::BASE64URL_NOPAD;
|
||||||
use fs_err as fs;
|
use fs_err as fs;
|
||||||
use fs_err::{DirEntry, File};
|
use fs_err::{DirEntry, File};
|
||||||
|
@ -27,7 +26,7 @@ use uv_normalize::PackageName;
|
||||||
|
|
||||||
use crate::install_location::{InstallLocation, LockedDir};
|
use crate::install_location::{InstallLocation, LockedDir};
|
||||||
use crate::record::RecordEntry;
|
use crate::record::RecordEntry;
|
||||||
use crate::script::Script;
|
use crate::script::{scripts_from_ini, Script};
|
||||||
use crate::{find_dist_info, Error};
|
use crate::{find_dist_info, Error};
|
||||||
|
|
||||||
/// `#!/usr/bin/env python`
|
/// `#!/usr/bin/env python`
|
||||||
|
@ -107,32 +106,16 @@ fn parse_scripts<R: Read + Seek>(
|
||||||
archive: &mut ZipArchive<R>,
|
archive: &mut ZipArchive<R>,
|
||||||
dist_info_dir: &str,
|
dist_info_dir: &str,
|
||||||
extras: Option<&[String]>,
|
extras: Option<&[String]>,
|
||||||
|
python_minor: u8,
|
||||||
) -> Result<(Vec<Script>, Vec<Script>), Error> {
|
) -> Result<(Vec<Script>, Vec<Script>), Error> {
|
||||||
let entry_points_path = format!("{dist_info_dir}/entry_points.txt");
|
let entry_points_path = format!("{dist_info_dir}/entry_points.txt");
|
||||||
let entry_points_mapping = match archive.by_name(&entry_points_path) {
|
let ini = match archive.by_name(&entry_points_path) {
|
||||||
Ok(file) => {
|
Ok(file) => std::io::read_to_string(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}")))?
|
|
||||||
}
|
|
||||||
Err(ZipError::FileNotFound) => return Ok((Vec::new(), Vec::new())),
|
Err(ZipError::FileNotFound) => return Ok((Vec::new(), Vec::new())),
|
||||||
Err(err) => return Err(Error::Zip(entry_points_path, err)),
|
Err(err) => return Err(Error::Zip(entry_points_path, err)),
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: handle extras
|
scripts_from_ini(extras, python_minor, ini)
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shamelessly stolen (and updated for recent sha2)
|
/// Shamelessly stolen (and updated for recent sha2)
|
||||||
|
@ -1045,7 +1028,12 @@ pub fn install_wheel(
|
||||||
);
|
);
|
||||||
|
|
||||||
debug!(name = name.as_str(), "Writing entrypoints");
|
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(
|
write_script_entrypoints(
|
||||||
&site_packages,
|
&site_packages,
|
||||||
location,
|
location,
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
#![cfg(all(feature = "python", feature = "pypi"))]
|
#![cfg(all(feature = "python", feature = "pypi"))]
|
||||||
|
|
||||||
|
use std::env::consts::EXE_SUFFIX;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
@ -2757,3 +2758,44 @@ fn set_read_permissions() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
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