mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-30 23:37:24 +00:00
Install versioned Python executables into the bin directory during uv python install
(#8458)
Some checks are pending
CI / check system | python on macos x86_64 (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / build binary | linux (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | python3.10 on windows (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on linux (push) Blocked by required conditions
CI / check system | conda3.8 on linux (push) Blocked by required conditions
CI / check system | conda3.11 on macos (push) Blocked by required conditions
CI / check system | conda3.8 on macos (push) Blocked by required conditions
CI / check system | conda3.11 on windows (push) Blocked by required conditions
CI / check system | conda3.8 on windows (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Some checks are pending
CI / check system | python on macos x86_64 (push) Blocked by required conditions
CI / cargo dev generate-all (push) Blocked by required conditions
CI / cargo shear (push) Waiting to run
CI / Determine changes (push) Waiting to run
CI / lint (push) Waiting to run
CI / cargo clippy | ubuntu (push) Blocked by required conditions
CI / cargo clippy | windows (push) Blocked by required conditions
CI / cargo test | ubuntu (push) Blocked by required conditions
CI / cargo test | macos (push) Blocked by required conditions
CI / cargo test | windows (push) Blocked by required conditions
CI / check windows trampoline | aarch64 (push) Blocked by required conditions
CI / integration test | pypy on ubuntu (push) Blocked by required conditions
CI / check windows trampoline | i686 (push) Blocked by required conditions
CI / check windows trampoline | x86_64 (push) Blocked by required conditions
CI / test windows trampoline | i686 (push) Blocked by required conditions
CI / test windows trampoline | x86_64 (push) Blocked by required conditions
CI / typos (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / build binary | linux (push) Blocked by required conditions
CI / build binary | macos aarch64 (push) Blocked by required conditions
CI / build binary | macos x86_64 (push) Blocked by required conditions
CI / build binary | windows (push) Blocked by required conditions
CI / build binary | freebsd (push) Blocked by required conditions
CI / integration test | pypy on windows (push) Blocked by required conditions
CI / ecosystem test | prefecthq/prefect (push) Blocked by required conditions
CI / ecosystem test | pallets/flask (push) Blocked by required conditions
CI / integration test | conda on ubuntu (push) Blocked by required conditions
CI / integration test | free-threaded on linux (push) Blocked by required conditions
CI / integration test | free-threaded on windows (push) Blocked by required conditions
CI / integration test | graalpy on ubuntu (push) Blocked by required conditions
CI / integration test | graalpy on windows (push) Blocked by required conditions
CI / integration test | github actions (push) Blocked by required conditions
CI / integration test | determine publish changes (push) Blocked by required conditions
CI / integration test | uv publish (push) Blocked by required conditions
CI / check cache | ubuntu (push) Blocked by required conditions
CI / check cache | macos aarch64 (push) Blocked by required conditions
CI / check system | python on debian (push) Blocked by required conditions
CI / check system | python on fedora (push) Blocked by required conditions
CI / check system | python on ubuntu (push) Blocked by required conditions
CI / check system | python on opensuse (push) Blocked by required conditions
CI / check system | homebrew python on macos aarch64 (push) Blocked by required conditions
CI / check system | python on rocky linux 8 (push) Blocked by required conditions
CI / check system | python on rocky linux 9 (push) Blocked by required conditions
CI / check system | pypy on ubuntu (push) Blocked by required conditions
CI / check system | pyston (push) Blocked by required conditions
CI / check system | alpine (push) Blocked by required conditions
CI / check system | python on macos aarch64 (push) Blocked by required conditions
CI / check system | python3.10 on windows (push) Blocked by required conditions
CI / check system | python3.10 on windows x86 (push) Blocked by required conditions
CI / check system | python3.13 on windows (push) Blocked by required conditions
CI / check system | python3.12 via chocolatey (push) Blocked by required conditions
CI / check system | python3.9 via pyenv (push) Blocked by required conditions
CI / check system | python3.13 (push) Blocked by required conditions
CI / check system | conda3.11 on linux (push) Blocked by required conditions
CI / check system | conda3.8 on linux (push) Blocked by required conditions
CI / check system | conda3.11 on macos (push) Blocked by required conditions
CI / check system | conda3.8 on macos (push) Blocked by required conditions
CI / check system | conda3.11 on windows (push) Blocked by required conditions
CI / check system | conda3.8 on windows (push) Blocked by required conditions
CI / check system | amazonlinux (push) Blocked by required conditions
CI / check system | embedded python3.10 on windows (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
Updates `uv python install` to link `python3.x` in the executable directory (i.e., `~/.local/bin`) to the the managed interpreter path. Includes - #8569 - #8571 Remaining work - #8663 - #8650 - Add an opt-out setting and flag - Update documentation
This commit is contained in:
parent
94fc35edd9
commit
4dd36b799f
19 changed files with 597 additions and 89 deletions
|
@ -20,6 +20,7 @@ uv-cache = { workspace = true }
|
|||
uv-cache-info = { workspace = true }
|
||||
uv-cache-key = { workspace = true }
|
||||
uv-client = { workspace = true }
|
||||
uv-dirs = { workspace = true }
|
||||
uv-distribution-filename = { workspace = true }
|
||||
uv-extract = { workspace = true }
|
||||
uv-fs = { workspace = true }
|
||||
|
|
|
@ -1236,6 +1236,16 @@ impl PythonVariant {
|
|||
PythonVariant::Freethreaded => interpreter.gil_disabled(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the lib or executable suffix for the variant, e.g., `t` for `python3.13t`.
|
||||
///
|
||||
/// Returns an empty string for the default Python variant.
|
||||
pub fn suffix(self) -> &'static str {
|
||||
match self {
|
||||
Self::Default => "",
|
||||
Self::Freethreaded => "t",
|
||||
}
|
||||
}
|
||||
}
|
||||
impl PythonRequest {
|
||||
/// Create a request from a string.
|
||||
|
@ -1635,12 +1645,7 @@ impl std::fmt::Display for ExecutableName {
|
|||
if let Some(prerelease) = &self.prerelease {
|
||||
write!(f, "{prerelease}")?;
|
||||
}
|
||||
match self.variant {
|
||||
PythonVariant::Default => {}
|
||||
PythonVariant::Freethreaded => {
|
||||
f.write_str("t")?;
|
||||
}
|
||||
};
|
||||
f.write_str(self.variant.suffix())?;
|
||||
f.write_str(std::env::consts::EXE_SUFFIX)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -305,6 +305,17 @@ impl PythonInstallationKey {
|
|||
pub fn libc(&self) -> &Libc {
|
||||
&self.libc
|
||||
}
|
||||
|
||||
/// Return a canonical name for a versioned executable.
|
||||
pub fn versioned_executable_name(&self) -> String {
|
||||
format!(
|
||||
"python{maj}.{min}{var}{exe}",
|
||||
maj = self.major,
|
||||
min = self.minor,
|
||||
var = self.variant.suffix(),
|
||||
exe = std::env::consts::EXE_SUFFIX
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for PythonInstallationKey {
|
||||
|
|
|
@ -53,14 +53,31 @@ pub enum Error {
|
|||
#[source]
|
||||
err: io::Error,
|
||||
},
|
||||
#[error("Failed to create Python executable link at {} from {}", to.user_display(), from.user_display())]
|
||||
LinkExecutable {
|
||||
from: PathBuf,
|
||||
to: PathBuf,
|
||||
#[source]
|
||||
err: io::Error,
|
||||
},
|
||||
#[error("Failed to create directory for Python executable link at {}", to.user_display())]
|
||||
ExecutableDirectory {
|
||||
to: PathBuf,
|
||||
#[source]
|
||||
err: io::Error,
|
||||
},
|
||||
#[error("Failed to read Python installation directory: {0}", dir.user_display())]
|
||||
ReadError {
|
||||
dir: PathBuf,
|
||||
#[source]
|
||||
err: io::Error,
|
||||
},
|
||||
#[error("Failed to find a directory to install executables into")]
|
||||
NoExecutableDirectory,
|
||||
#[error("Failed to read managed Python directory name: {0}")]
|
||||
NameError(String),
|
||||
#[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
|
||||
AbsolutePath(PathBuf, #[source] std::io::Error),
|
||||
#[error(transparent)]
|
||||
NameParseError(#[from] installation::PythonInstallationKeyError),
|
||||
#[error(transparent)]
|
||||
|
@ -267,18 +284,78 @@ impl ManagedPythonInstallation {
|
|||
.ok_or(Error::NameError("not a valid string".to_string()))?,
|
||||
)?;
|
||||
|
||||
let path = std::path::absolute(&path).map_err(|err| Error::AbsolutePath(path, err))?;
|
||||
|
||||
Ok(Self { path, key })
|
||||
}
|
||||
|
||||
/// The path to this toolchain's Python executable.
|
||||
/// The path to this managed installation's Python executable.
|
||||
///
|
||||
/// If the installation has multiple execututables i.e., `python`, `python3`, etc., this will
|
||||
/// return the _canonical_ executable name which the other names link to. On Unix, this is
|
||||
/// `python{major}.{minor}{variant}` and on Windows, this is `python{exe}`.
|
||||
pub fn executable(&self) -> PathBuf {
|
||||
if cfg!(windows) {
|
||||
self.python_dir().join("python.exe")
|
||||
let implementation = match self.implementation() {
|
||||
ImplementationName::CPython => "python",
|
||||
ImplementationName::PyPy => "pypy",
|
||||
ImplementationName::GraalPy => {
|
||||
unreachable!("Managed installations of GraalPy are not supported")
|
||||
}
|
||||
};
|
||||
|
||||
let version = match self.implementation() {
|
||||
ImplementationName::CPython => {
|
||||
if cfg!(unix) {
|
||||
format!("{}.{}", self.key.major, self.key.minor)
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
// PyPy uses a full version number, even on Windows.
|
||||
ImplementationName::PyPy => format!("{}.{}", self.key.major, self.key.minor),
|
||||
ImplementationName::GraalPy => {
|
||||
unreachable!("Managed installations of GraalPy are not supported")
|
||||
}
|
||||
};
|
||||
|
||||
// On Windows, the executable is just `python.exe` even for alternative variants
|
||||
let variant = if cfg!(unix) {
|
||||
self.key.variant.suffix()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let name = format!(
|
||||
"{implementation}{version}{variant}{exe}",
|
||||
exe = std::env::consts::EXE_SUFFIX
|
||||
);
|
||||
|
||||
let executable = if cfg!(windows) {
|
||||
self.python_dir().join(name)
|
||||
} else if cfg!(unix) {
|
||||
self.python_dir().join("bin").join("python3")
|
||||
self.python_dir().join("bin").join(name)
|
||||
} else {
|
||||
unimplemented!("Only Windows and Unix systems are supported.")
|
||||
};
|
||||
|
||||
// Workaround for python-build-standalone v20241016 which is missing the standard
|
||||
// `python.exe` executable in free-threaded distributions on Windows.
|
||||
//
|
||||
// See https://github.com/astral-sh/uv/issues/8298
|
||||
if cfg!(windows)
|
||||
&& matches!(self.key.variant, PythonVariant::Freethreaded)
|
||||
&& !executable.exists()
|
||||
{
|
||||
// This is the alternative executable name for the freethreaded variant
|
||||
return self.python_dir().join(format!(
|
||||
"python{}.{}t{}",
|
||||
self.key.major,
|
||||
self.key.minor,
|
||||
std::env::consts::EXE_SUFFIX
|
||||
));
|
||||
}
|
||||
|
||||
executable
|
||||
}
|
||||
|
||||
fn python_dir(&self) -> PathBuf {
|
||||
|
@ -336,39 +413,38 @@ impl ManagedPythonInstallation {
|
|||
pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
|
||||
let python = self.executable();
|
||||
|
||||
// Workaround for python-build-standalone v20241016 which is missing the standard
|
||||
// `python.exe` executable in free-threaded distributions on Windows.
|
||||
//
|
||||
// See https://github.com/astral-sh/uv/issues/8298
|
||||
if !python.try_exists()? {
|
||||
match self.key.variant {
|
||||
PythonVariant::Default => return Err(Error::MissingExecutable(python.clone())),
|
||||
PythonVariant::Freethreaded => {
|
||||
// This is the alternative executable name for the freethreaded variant
|
||||
let python_in_dist = self.python_dir().join(format!(
|
||||
"python{}.{}t{}",
|
||||
self.key.major,
|
||||
self.key.minor,
|
||||
std::env::consts::EXE_SUFFIX
|
||||
));
|
||||
debug!(
|
||||
"Creating link {} -> {}",
|
||||
python.user_display(),
|
||||
python_in_dist.user_display()
|
||||
);
|
||||
uv_fs::symlink_copy_fallback_file(&python_in_dist, &python).map_err(|err| {
|
||||
if err.kind() == io::ErrorKind::NotFound {
|
||||
Error::MissingExecutable(python_in_dist.clone())
|
||||
} else {
|
||||
Error::CanonicalizeExecutable {
|
||||
from: python_in_dist,
|
||||
to: python,
|
||||
err,
|
||||
}
|
||||
}
|
||||
})?;
|
||||
}
|
||||
let canonical_names = &["python"];
|
||||
|
||||
for name in canonical_names {
|
||||
let executable =
|
||||
python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
|
||||
|
||||
// Do not attempt to perform same-file copies — this is fine on Unix but fails on
|
||||
// Windows with a permission error instead of 'already exists'
|
||||
if executable == python {
|
||||
continue;
|
||||
}
|
||||
|
||||
match uv_fs::symlink_copy_fallback_file(&python, &executable) {
|
||||
Ok(()) => {
|
||||
debug!(
|
||||
"Created link {} -> {}",
|
||||
executable.user_display(),
|
||||
python.user_display(),
|
||||
);
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
return Err(Error::MissingExecutable(python.clone()))
|
||||
}
|
||||
Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
|
||||
Err(err) => {
|
||||
return Err(Error::CanonicalizeExecutable {
|
||||
from: executable,
|
||||
to: python,
|
||||
err,
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
@ -381,10 +457,7 @@ impl ManagedPythonInstallation {
|
|||
let stdlib = if matches!(self.key.os, Os(target_lexicon::OperatingSystem::Windows)) {
|
||||
self.python_dir().join("Lib")
|
||||
} else {
|
||||
let lib_suffix = match self.key.variant {
|
||||
PythonVariant::Default => "",
|
||||
PythonVariant::Freethreaded => "t",
|
||||
};
|
||||
let lib_suffix = self.key.variant.suffix();
|
||||
let python = if matches!(
|
||||
self.key.implementation,
|
||||
LenientImplementationName::Known(ImplementationName::PyPy)
|
||||
|
@ -401,6 +474,31 @@ impl ManagedPythonInstallation {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a link to the Python executable in the given `bin` directory.
|
||||
pub fn create_bin_link(&self, bin: &Path) -> Result<PathBuf, Error> {
|
||||
let python = self.executable();
|
||||
|
||||
fs_err::create_dir_all(bin).map_err(|err| Error::ExecutableDirectory {
|
||||
to: bin.to_path_buf(),
|
||||
err,
|
||||
})?;
|
||||
|
||||
// TODO(zanieb): Add support for a "default" which
|
||||
let python_in_bin = bin.join(self.key.versioned_executable_name());
|
||||
|
||||
match uv_fs::symlink_copy_fallback_file(&python, &python_in_bin) {
|
||||
Ok(()) => Ok(python_in_bin),
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
Err(Error::MissingExecutable(python.clone()))
|
||||
}
|
||||
Err(err) => Err(Error::LinkExecutable {
|
||||
from: python,
|
||||
to: python_in_bin,
|
||||
err,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a platform portion of a key from the environment.
|
||||
|
@ -423,3 +521,9 @@ impl fmt::Display for ManagedPythonInstallation {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the directory to install Python executables into.
|
||||
pub fn python_executable_dir() -> Result<PathBuf, Error> {
|
||||
uv_dirs::user_executable_directory(Some(EnvVars::UV_PYTHON_BIN_DIR))
|
||||
.ok_or(Error::NoExecutableDirectory)
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue