mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-02 18:12:17 +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
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -702,7 +702,7 @@ jobs:
|
||||||
|
|
||||||
- name: "Install free-threaded Python via uv"
|
- name: "Install free-threaded Python via uv"
|
||||||
run: |
|
run: |
|
||||||
./uv python install 3.13t
|
./uv python install -v 3.13t
|
||||||
./uv venv -p 3.13t --python-preference only-managed
|
./uv venv -p 3.13t --python-preference only-managed
|
||||||
|
|
||||||
- name: "Check version"
|
- name: "Check version"
|
||||||
|
@ -774,7 +774,7 @@ jobs:
|
||||||
run: chmod +x ./uv
|
run: chmod +x ./uv
|
||||||
|
|
||||||
- name: "Install PyPy"
|
- name: "Install PyPy"
|
||||||
run: ./uv python install pypy3.9
|
run: ./uv python install -v pypy3.9
|
||||||
|
|
||||||
- name: "Create a virtual environment"
|
- name: "Create a virtual environment"
|
||||||
run: |
|
run: |
|
||||||
|
|
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4208,6 +4208,7 @@ dependencies = [
|
||||||
"regex",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
|
"same-file",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"similar",
|
"similar",
|
||||||
|
@ -5060,6 +5061,7 @@ dependencies = [
|
||||||
"uv-cache-info",
|
"uv-cache-info",
|
||||||
"uv-cache-key",
|
"uv-cache-key",
|
||||||
"uv-client",
|
"uv-client",
|
||||||
|
"uv-dirs",
|
||||||
"uv-distribution-filename",
|
"uv-distribution-filename",
|
||||||
"uv-extract",
|
"uv-extract",
|
||||||
"uv-fs",
|
"uv-fs",
|
||||||
|
|
|
@ -3803,14 +3803,15 @@ pub enum PythonCommand {
|
||||||
///
|
///
|
||||||
/// Multiple Python versions may be requested.
|
/// Multiple Python versions may be requested.
|
||||||
///
|
///
|
||||||
/// Supports CPython and PyPy.
|
/// Supports CPython and PyPy. CPython distributions are downloaded from the
|
||||||
|
/// `python-build-standalone` project. PyPy distributions are downloaded from `python.org`.
|
||||||
///
|
///
|
||||||
/// CPython distributions are downloaded from the `python-build-standalone` project.
|
/// Python versions are installed into the uv Python directory, which can be retrieved with `uv
|
||||||
|
/// python dir`.
|
||||||
///
|
///
|
||||||
/// Python versions are installed into the uv Python directory, which can be
|
/// A `python` executable is not made globally available, managed Python versions are only used
|
||||||
/// retrieved with `uv python dir`. A `python` executable is not made
|
/// in uv commands or in active virtual environments. There is experimental support for
|
||||||
/// globally available, managed Python versions are only used in uv
|
/// adding Python executables to the `PATH` — use the `--preview` flag to enable this behavior.
|
||||||
/// commands or in active virtual environments.
|
|
||||||
///
|
///
|
||||||
/// See `uv help python` to view supported request formats.
|
/// See `uv help python` to view supported request formats.
|
||||||
Install(PythonInstallArgs),
|
Install(PythonInstallArgs),
|
||||||
|
@ -3838,7 +3839,9 @@ pub enum PythonCommand {
|
||||||
/// `%APPDATA%\uv\data\python` on Windows.
|
/// `%APPDATA%\uv\data\python` on Windows.
|
||||||
///
|
///
|
||||||
/// The Python installation directory may be overridden with `$UV_PYTHON_INSTALL_DIR`.
|
/// The Python installation directory may be overridden with `$UV_PYTHON_INSTALL_DIR`.
|
||||||
Dir,
|
///
|
||||||
|
/// To instead view the directory uv installs Python executables into, use the `--bin` flag.
|
||||||
|
Dir(PythonDirArgs),
|
||||||
|
|
||||||
/// Uninstall Python versions.
|
/// Uninstall Python versions.
|
||||||
Uninstall(PythonUninstallArgs),
|
Uninstall(PythonUninstallArgs),
|
||||||
|
@ -3866,6 +3869,27 @@ pub struct PythonListArgs {
|
||||||
pub only_installed: bool,
|
pub only_installed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
pub struct PythonDirArgs {
|
||||||
|
/// Show the directory into which `uv python` will install Python executables.
|
||||||
|
///
|
||||||
|
/// Note this directory is only used when installing with preview mode enabled.
|
||||||
|
///
|
||||||
|
/// By default, `uv python dir` shows the directory into which the Python distributions
|
||||||
|
/// themselves are installed, rather than the directory containing the linked executables.
|
||||||
|
///
|
||||||
|
/// The Python executable directory is determined according to the XDG standard and is derived
|
||||||
|
/// from the following environment variables, in order of preference:
|
||||||
|
///
|
||||||
|
/// - `$UV_PYTHON_BIN_DIR`
|
||||||
|
/// - `$XDG_BIN_HOME`
|
||||||
|
/// - `$XDG_DATA_HOME/../bin`
|
||||||
|
/// - `$HOME/.local/bin`
|
||||||
|
#[arg(long, verbatim_doc_comment)]
|
||||||
|
pub bin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Args)]
|
#[derive(Args)]
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
pub struct PythonInstallArgs {
|
pub struct PythonInstallArgs {
|
||||||
|
|
|
@ -20,6 +20,7 @@ uv-cache = { workspace = true }
|
||||||
uv-cache-info = { workspace = true }
|
uv-cache-info = { workspace = true }
|
||||||
uv-cache-key = { workspace = true }
|
uv-cache-key = { workspace = true }
|
||||||
uv-client = { workspace = true }
|
uv-client = { workspace = true }
|
||||||
|
uv-dirs = { workspace = true }
|
||||||
uv-distribution-filename = { workspace = true }
|
uv-distribution-filename = { workspace = true }
|
||||||
uv-extract = { workspace = true }
|
uv-extract = { workspace = true }
|
||||||
uv-fs = { workspace = true }
|
uv-fs = { workspace = true }
|
||||||
|
|
|
@ -1236,6 +1236,16 @@ impl PythonVariant {
|
||||||
PythonVariant::Freethreaded => interpreter.gil_disabled(),
|
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 {
|
impl PythonRequest {
|
||||||
/// Create a request from a string.
|
/// Create a request from a string.
|
||||||
|
@ -1635,12 +1645,7 @@ impl std::fmt::Display for ExecutableName {
|
||||||
if let Some(prerelease) = &self.prerelease {
|
if let Some(prerelease) = &self.prerelease {
|
||||||
write!(f, "{prerelease}")?;
|
write!(f, "{prerelease}")?;
|
||||||
}
|
}
|
||||||
match self.variant {
|
f.write_str(self.variant.suffix())?;
|
||||||
PythonVariant::Default => {}
|
|
||||||
PythonVariant::Freethreaded => {
|
|
||||||
f.write_str("t")?;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
f.write_str(std::env::consts::EXE_SUFFIX)?;
|
f.write_str(std::env::consts::EXE_SUFFIX)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -305,6 +305,17 @@ impl PythonInstallationKey {
|
||||||
pub fn libc(&self) -> &Libc {
|
pub fn libc(&self) -> &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 {
|
impl fmt::Display for PythonInstallationKey {
|
||||||
|
|
|
@ -53,14 +53,31 @@ pub enum Error {
|
||||||
#[source]
|
#[source]
|
||||||
err: io::Error,
|
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())]
|
#[error("Failed to read Python installation directory: {0}", dir.user_display())]
|
||||||
ReadError {
|
ReadError {
|
||||||
dir: PathBuf,
|
dir: PathBuf,
|
||||||
#[source]
|
#[source]
|
||||||
err: io::Error,
|
err: io::Error,
|
||||||
},
|
},
|
||||||
|
#[error("Failed to find a directory to install executables into")]
|
||||||
|
NoExecutableDirectory,
|
||||||
#[error("Failed to read managed Python directory name: {0}")]
|
#[error("Failed to read managed Python directory name: {0}")]
|
||||||
NameError(String),
|
NameError(String),
|
||||||
|
#[error("Failed to construct absolute path to managed Python directory: {}", _0.user_display())]
|
||||||
|
AbsolutePath(PathBuf, #[source] std::io::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
NameParseError(#[from] installation::PythonInstallationKeyError),
|
NameParseError(#[from] installation::PythonInstallationKeyError),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
@ -267,18 +284,78 @@ impl ManagedPythonInstallation {
|
||||||
.ok_or(Error::NameError("not a valid string".to_string()))?,
|
.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 })
|
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 {
|
pub fn executable(&self) -> PathBuf {
|
||||||
if cfg!(windows) {
|
let implementation = match self.implementation() {
|
||||||
self.python_dir().join("python.exe")
|
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) {
|
} else if cfg!(unix) {
|
||||||
self.python_dir().join("bin").join("python3")
|
self.python_dir().join("bin").join(name)
|
||||||
} else {
|
} else {
|
||||||
unimplemented!("Only Windows and Unix systems are supported.")
|
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 {
|
fn python_dir(&self) -> PathBuf {
|
||||||
|
@ -336,39 +413,38 @@ impl ManagedPythonInstallation {
|
||||||
pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
|
pub fn ensure_canonical_executables(&self) -> Result<(), Error> {
|
||||||
let python = self.executable();
|
let python = self.executable();
|
||||||
|
|
||||||
// Workaround for python-build-standalone v20241016 which is missing the standard
|
let canonical_names = &["python"];
|
||||||
// `python.exe` executable in free-threaded distributions on Windows.
|
|
||||||
//
|
for name in canonical_names {
|
||||||
// See https://github.com/astral-sh/uv/issues/8298
|
let executable =
|
||||||
if !python.try_exists()? {
|
python.with_file_name(format!("{name}{exe}", exe = std::env::consts::EXE_SUFFIX));
|
||||||
match self.key.variant {
|
|
||||||
PythonVariant::Default => return Err(Error::MissingExecutable(python.clone())),
|
// Do not attempt to perform same-file copies — this is fine on Unix but fails on
|
||||||
PythonVariant::Freethreaded => {
|
// Windows with a permission error instead of 'already exists'
|
||||||
// This is the alternative executable name for the freethreaded variant
|
if executable == python {
|
||||||
let python_in_dist = self.python_dir().join(format!(
|
continue;
|
||||||
"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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(())
|
Ok(())
|
||||||
|
@ -381,10 +457,7 @@ impl ManagedPythonInstallation {
|
||||||
let stdlib = if matches!(self.key.os, Os(target_lexicon::OperatingSystem::Windows)) {
|
let stdlib = if matches!(self.key.os, Os(target_lexicon::OperatingSystem::Windows)) {
|
||||||
self.python_dir().join("Lib")
|
self.python_dir().join("Lib")
|
||||||
} else {
|
} else {
|
||||||
let lib_suffix = match self.key.variant {
|
let lib_suffix = self.key.variant.suffix();
|
||||||
PythonVariant::Default => "",
|
|
||||||
PythonVariant::Freethreaded => "t",
|
|
||||||
};
|
|
||||||
let python = if matches!(
|
let python = if matches!(
|
||||||
self.key.implementation,
|
self.key.implementation,
|
||||||
LenientImplementationName::Known(ImplementationName::PyPy)
|
LenientImplementationName::Known(ImplementationName::PyPy)
|
||||||
|
@ -401,6 +474,31 @@ impl ManagedPythonInstallation {
|
||||||
|
|
||||||
Ok(())
|
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.
|
/// 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)
|
||||||
|
}
|
||||||
|
|
|
@ -141,6 +141,9 @@ impl EnvVars {
|
||||||
/// Specifies the path to the project virtual environment.
|
/// Specifies the path to the project virtual environment.
|
||||||
pub const UV_PROJECT_ENVIRONMENT: &'static str = "UV_PROJECT_ENVIRONMENT";
|
pub const UV_PROJECT_ENVIRONMENT: &'static str = "UV_PROJECT_ENVIRONMENT";
|
||||||
|
|
||||||
|
/// Specifies the directory to place links to installed, managed Python executables.
|
||||||
|
pub const UV_PYTHON_BIN_DIR: &'static str = "UV_PYTHON_BIN_DIR";
|
||||||
|
|
||||||
/// Specifies the directory for storing managed Python installations.
|
/// Specifies the directory for storing managed Python installations.
|
||||||
pub const UV_PYTHON_INSTALL_DIR: &'static str = "UV_PYTHON_INSTALL_DIR";
|
pub const UV_PYTHON_INSTALL_DIR: &'static str = "UV_PYTHON_INSTALL_DIR";
|
||||||
|
|
||||||
|
|
|
@ -42,7 +42,7 @@ pub enum Error {
|
||||||
EntrypointRead(#[from] uv_install_wheel::Error),
|
EntrypointRead(#[from] uv_install_wheel::Error),
|
||||||
#[error("Failed to find dist-info directory `{0}` in environment at {1}")]
|
#[error("Failed to find dist-info directory `{0}` in environment at {1}")]
|
||||||
DistInfoMissing(String, PathBuf),
|
DistInfoMissing(String, PathBuf),
|
||||||
#[error("Failed to find a directory for executables")]
|
#[error("Failed to find a directory to install executables into")]
|
||||||
NoExecutableDirectory,
|
NoExecutableDirectory,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
ToolName(#[from] InvalidNameError),
|
ToolName(#[from] InvalidNameError),
|
||||||
|
|
|
@ -78,6 +78,7 @@ rayon = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
reqwest = { workspace = true }
|
reqwest = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
|
same-file = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
|
@ -3,15 +3,21 @@ use anyhow::Context;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
|
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_python::managed::ManagedPythonInstallations;
|
use uv_python::managed::{python_executable_dir, ManagedPythonInstallations};
|
||||||
|
|
||||||
|
/// Show the Python installation directory.
|
||||||
|
pub(crate) fn dir(bin: bool) -> anyhow::Result<()> {
|
||||||
|
if bin {
|
||||||
|
let bin = python_executable_dir()?;
|
||||||
|
println!("{}", bin.simplified_display().cyan());
|
||||||
|
} else {
|
||||||
|
let installed_toolchains = ManagedPythonInstallations::from_settings()
|
||||||
|
.context("Failed to initialize toolchain settings")?;
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
installed_toolchains.root().simplified_display().cyan()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Show the toolchain directory.
|
|
||||||
pub(crate) fn dir() -> anyhow::Result<()> {
|
|
||||||
let installed_toolchains = ManagedPythonInstallations::from_settings()
|
|
||||||
.context("Failed to initialize toolchain settings")?;
|
|
||||||
println!(
|
|
||||||
"{}",
|
|
||||||
installed_toolchains.root().simplified_display().cyan()
|
|
||||||
);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,27 @@
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::fmt::Write;
|
||||||
|
use std::io::ErrorKind;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use futures::stream::FuturesUnordered;
|
use futures::stream::FuturesUnordered;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use std::collections::BTreeSet;
|
use same_file::is_same_file;
|
||||||
use std::fmt::Write;
|
|
||||||
use std::path::Path;
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use uv_client::Connectivity;
|
use uv_client::Connectivity;
|
||||||
|
use uv_configuration::PreviewMode;
|
||||||
|
use uv_fs::Simplified;
|
||||||
use uv_python::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest};
|
use uv_python::downloads::{DownloadResult, ManagedPythonDownload, PythonDownloadRequest};
|
||||||
use uv_python::managed::{ManagedPythonInstallation, ManagedPythonInstallations};
|
use uv_python::managed::{
|
||||||
|
python_executable_dir, ManagedPythonInstallation, ManagedPythonInstallations,
|
||||||
|
};
|
||||||
use uv_python::{PythonDownloads, PythonRequest, PythonVersionFile};
|
use uv_python::{PythonDownloads, PythonRequest, PythonVersionFile};
|
||||||
|
use uv_shell::Shell;
|
||||||
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
use crate::commands::python::{ChangeEvent, ChangeEventKind};
|
use crate::commands::python::{ChangeEvent, ChangeEventKind};
|
||||||
use crate::commands::reporters::PythonDownloadReporter;
|
use crate::commands::reporters::PythonDownloadReporter;
|
||||||
|
@ -28,6 +37,7 @@ pub(crate) async fn install(
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
no_config: bool,
|
no_config: bool,
|
||||||
|
preview: PreviewMode,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
) -> Result<ExitStatus> {
|
) -> Result<ExitStatus> {
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
@ -156,6 +166,12 @@ pub(crate) async fn install(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let bin = if preview.is_enabled() {
|
||||||
|
Some(python_executable_dir()?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let mut installed = FxHashSet::default();
|
let mut installed = FxHashSet::default();
|
||||||
let mut errors = vec![];
|
let mut errors = vec![];
|
||||||
while let Some((key, result)) = tasks.next().await {
|
while let Some((key, result)) = tasks.next().await {
|
||||||
|
@ -173,9 +189,46 @@ pub(crate) async fn install(
|
||||||
let managed = ManagedPythonInstallation::new(path.clone())?;
|
let managed = ManagedPythonInstallation::new(path.clone())?;
|
||||||
managed.ensure_externally_managed()?;
|
managed.ensure_externally_managed()?;
|
||||||
managed.ensure_canonical_executables()?;
|
managed.ensure_canonical_executables()?;
|
||||||
|
|
||||||
|
if preview.is_enabled() && cfg!(unix) {
|
||||||
|
let bin = bin
|
||||||
|
.as_ref()
|
||||||
|
.expect("We should have a bin directory with preview enabled")
|
||||||
|
.as_path();
|
||||||
|
match managed.create_bin_link(bin) {
|
||||||
|
Ok(executable) => {
|
||||||
|
debug!("Installed {} executable to {}", key, executable.display());
|
||||||
|
}
|
||||||
|
Err(uv_python::managed::Error::LinkExecutable { from, to, err })
|
||||||
|
if err.kind() == ErrorKind::AlreadyExists =>
|
||||||
|
{
|
||||||
|
// TODO(zanieb): Add `--force`
|
||||||
|
if reinstall {
|
||||||
|
fs_err::remove_file(&to)?;
|
||||||
|
let executable = managed.create_bin_link(bin)?;
|
||||||
|
debug!(
|
||||||
|
"Replaced {} executable at {}",
|
||||||
|
key,
|
||||||
|
executable.user_display()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if !is_same_file(&to, &from).unwrap_or_default() {
|
||||||
|
errors.push((
|
||||||
|
key,
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"Executable already exists at `{}`. Use `--reinstall` to force replacement.",
|
||||||
|
to.user_display()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => return Err(err.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
errors.push((key, err));
|
errors.push((key, anyhow::Error::new(err)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -232,16 +285,42 @@ pub(crate) async fn install(
|
||||||
{
|
{
|
||||||
match event.kind {
|
match event.kind {
|
||||||
ChangeEventKind::Added => {
|
ChangeEventKind::Added => {
|
||||||
writeln!(printer.stderr(), " {} {}", "+".green(), event.key.bold())?;
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
" {} {} ({})",
|
||||||
|
"+".green(),
|
||||||
|
event.key.bold(),
|
||||||
|
event.key.versioned_executable_name()
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
ChangeEventKind::Removed => {
|
ChangeEventKind::Removed => {
|
||||||
writeln!(printer.stderr(), " {} {}", "-".red(), event.key.bold())?;
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
" {} {} ({})",
|
||||||
|
"-".red(),
|
||||||
|
event.key.bold(),
|
||||||
|
event.key.versioned_executable_name()
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
ChangeEventKind::Reinstalled => {
|
ChangeEventKind::Reinstalled => {
|
||||||
writeln!(printer.stderr(), " {} {}", "~".yellow(), event.key.bold(),)?;
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
" {} {} ({})",
|
||||||
|
"~".yellow(),
|
||||||
|
event.key.bold(),
|
||||||
|
event.key.versioned_executable_name()
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if preview.is_enabled() && cfg!(unix) {
|
||||||
|
let bin = bin
|
||||||
|
.as_ref()
|
||||||
|
.expect("We should have a bin directory with preview enabled")
|
||||||
|
.as_path();
|
||||||
|
warn_if_not_on_path(bin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.is_empty() {
|
if !errors.is_empty() {
|
||||||
|
@ -255,7 +334,7 @@ pub(crate) async fn install(
|
||||||
"error".red().bold(),
|
"error".red().bold(),
|
||||||
key.green()
|
key.green()
|
||||||
)?;
|
)?;
|
||||||
for err in anyhow::Error::new(err).chain() {
|
for err in err.chain() {
|
||||||
writeln!(
|
writeln!(
|
||||||
printer.stderr(),
|
printer.stderr(),
|
||||||
" {}: {}",
|
" {}: {}",
|
||||||
|
@ -269,3 +348,36 @@ pub(crate) async fn install(
|
||||||
|
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn warn_if_not_on_path(bin: &Path) {
|
||||||
|
if !Shell::contains_path(bin) {
|
||||||
|
if let Some(shell) = Shell::from_env() {
|
||||||
|
if let Some(command) = shell.prepend_path(bin) {
|
||||||
|
if shell.configuration_files().is_empty() {
|
||||||
|
warn_user!(
|
||||||
|
"`{}` is not on your PATH. To use the installed Python executable, run `{}`.",
|
||||||
|
bin.simplified_display().cyan(),
|
||||||
|
command.green()
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// TODO(zanieb): Update when we add `uv python update-shell` to match `uv tool`
|
||||||
|
warn_user!(
|
||||||
|
"`{}` is not on your PATH. To use the installed Python executable, run `{}`.",
|
||||||
|
bin.simplified_display().cyan(),
|
||||||
|
command.green(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn_user!(
|
||||||
|
"`{}` is not on your PATH. To use the installed Python executable, add the directory to your PATH.",
|
||||||
|
bin.simplified_display().cyan(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
warn_user!(
|
||||||
|
"`{}` is not on your PATH. To use the installed Python executable, add the directory to your PATH.",
|
||||||
|
bin.simplified_display().cyan(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,8 +7,11 @@ use futures::StreamExt;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
|
|
||||||
|
use same_file::is_same_file;
|
||||||
|
use tracing::{debug, warn};
|
||||||
|
use uv_fs::Simplified;
|
||||||
use uv_python::downloads::PythonDownloadRequest;
|
use uv_python::downloads::PythonDownloadRequest;
|
||||||
use uv_python::managed::ManagedPythonInstallations;
|
use uv_python::managed::{python_executable_dir, ManagedPythonInstallations};
|
||||||
use uv_python::PythonRequest;
|
use uv_python::PythonRequest;
|
||||||
|
|
||||||
use crate::commands::python::{ChangeEvent, ChangeEventKind};
|
use crate::commands::python::{ChangeEvent, ChangeEventKind};
|
||||||
|
@ -121,6 +124,40 @@ async fn do_uninstall(
|
||||||
return Ok(ExitStatus::Failure);
|
return Ok(ExitStatus::Failure);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect files in a directory
|
||||||
|
let executables = python_executable_dir()?
|
||||||
|
.read_dir()?
|
||||||
|
.filter_map(|entry| match entry {
|
||||||
|
Ok(entry) => Some(entry),
|
||||||
|
Err(err) => {
|
||||||
|
warn!("Failed to read executable: {}", err);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter(|entry| entry.file_type().is_ok_and(|file_type| !file_type.is_dir()))
|
||||||
|
.map(|entry| entry.path())
|
||||||
|
// Only include files that match the expected Python executable names
|
||||||
|
// TODO(zanieb): This is a minor optimization to avoid opening more files, but we could
|
||||||
|
// leave broken links behind, i.e., if the user created them.
|
||||||
|
.filter(|path| {
|
||||||
|
matching_installations.iter().any(|installation| {
|
||||||
|
path.file_name().and_then(|name| name.to_str())
|
||||||
|
== Some(&installation.key().versioned_executable_name())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
// Only include Python executables that match the installations
|
||||||
|
.filter(|path| {
|
||||||
|
matching_installations.iter().any(|installation| {
|
||||||
|
is_same_file(path, installation.executable()).unwrap_or_default()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
|
for executable in &executables {
|
||||||
|
fs_err::remove_file(executable)?;
|
||||||
|
debug!("Removed {}", executable.user_display());
|
||||||
|
}
|
||||||
|
|
||||||
let mut tasks = FuturesUnordered::new();
|
let mut tasks = FuturesUnordered::new();
|
||||||
for installation in &matching_installations {
|
for installation in &matching_installations {
|
||||||
tasks.push(async {
|
tasks.push(async {
|
||||||
|
@ -180,7 +217,13 @@ async fn do_uninstall(
|
||||||
{
|
{
|
||||||
match event.kind {
|
match event.kind {
|
||||||
ChangeEventKind::Removed => {
|
ChangeEventKind::Removed => {
|
||||||
writeln!(printer.stderr(), " {} {}", "-".red(), event.key.bold())?;
|
writeln!(
|
||||||
|
printer.stderr(),
|
||||||
|
" {} {} ({})",
|
||||||
|
"-".red(),
|
||||||
|
event.key.bold(),
|
||||||
|
event.key.versioned_executable_name()
|
||||||
|
)?;
|
||||||
}
|
}
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
|
|
@ -1057,6 +1057,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
globals.native_tls,
|
globals.native_tls,
|
||||||
globals.connectivity,
|
globals.connectivity,
|
||||||
cli.top_level.no_config,
|
cli.top_level.no_config,
|
||||||
|
globals.preview,
|
||||||
printer,
|
printer,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -1111,9 +1112,13 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
Commands::Python(PythonNamespace {
|
Commands::Python(PythonNamespace {
|
||||||
command: PythonCommand::Dir,
|
command: PythonCommand::Dir(args),
|
||||||
}) => {
|
}) => {
|
||||||
commands::python_dir()?;
|
// Resolve the settings from the command-line arguments and workspace configuration.
|
||||||
|
let args = settings::PythonDirSettings::resolve(args, filesystem);
|
||||||
|
show_settings!(args);
|
||||||
|
|
||||||
|
commands::python_dir(args.bin)?;
|
||||||
Ok(ExitStatus::Success)
|
Ok(ExitStatus::Success)
|
||||||
}
|
}
|
||||||
Commands::Publish(args) => {
|
Commands::Publish(args) => {
|
||||||
|
|
|
@ -8,7 +8,7 @@ use url::Url;
|
||||||
use uv_cache::{CacheArgs, Refresh};
|
use uv_cache::{CacheArgs, Refresh};
|
||||||
use uv_cli::{
|
use uv_cli::{
|
||||||
options::{flag, resolver_installer_options, resolver_options},
|
options::{flag, resolver_installer_options, resolver_options},
|
||||||
AuthorFrom, BuildArgs, ExportArgs, PublishArgs, ToolUpgradeArgs,
|
AuthorFrom, BuildArgs, ExportArgs, PublishArgs, PythonDirArgs, ToolUpgradeArgs,
|
||||||
};
|
};
|
||||||
use uv_cli::{
|
use uv_cli::{
|
||||||
AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe,
|
AddArgs, ColorChoice, ExternalCommand, GlobalArgs, InitArgs, ListFormat, LockArgs, Maybe,
|
||||||
|
@ -597,6 +597,23 @@ impl PythonListSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The resolved settings to use for a `python dir` invocation.
|
||||||
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub(crate) struct PythonDirSettings {
|
||||||
|
pub(crate) bin: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PythonDirSettings {
|
||||||
|
/// Resolve the [`PythonDirSettings`] from the CLI and filesystem configuration.
|
||||||
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
|
pub(crate) fn resolve(args: PythonDirArgs, _filesystem: Option<FilesystemOptions>) -> Self {
|
||||||
|
let PythonDirArgs { bin } = args;
|
||||||
|
|
||||||
|
Self { bin }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The resolved settings to use for a `python install` invocation.
|
/// The resolved settings to use for a `python install` invocation.
|
||||||
#[allow(clippy::struct_excessive_bools)]
|
#[allow(clippy::struct_excessive_bools)]
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
|
@ -653,11 +653,13 @@ impl TestContext {
|
||||||
pub fn python_install(&self) -> Command {
|
pub fn python_install(&self) -> Command {
|
||||||
let mut command = self.new_command();
|
let mut command = self.new_command();
|
||||||
let managed = self.temp_dir.join("managed");
|
let managed = self.temp_dir.join("managed");
|
||||||
|
let bin = self.temp_dir.join("bin");
|
||||||
self.add_shared_args(&mut command, true);
|
self.add_shared_args(&mut command, true);
|
||||||
command
|
command
|
||||||
.arg("python")
|
.arg("python")
|
||||||
.arg("install")
|
.arg("install")
|
||||||
.env(EnvVars::UV_PYTHON_INSTALL_DIR, managed)
|
.env(EnvVars::UV_PYTHON_INSTALL_DIR, managed)
|
||||||
|
.env(EnvVars::UV_PYTHON_BIN_DIR, bin)
|
||||||
.current_dir(&self.temp_dir);
|
.current_dir(&self.temp_dir);
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
@ -666,11 +668,13 @@ impl TestContext {
|
||||||
pub fn python_uninstall(&self) -> Command {
|
pub fn python_uninstall(&self) -> Command {
|
||||||
let mut command = self.new_command();
|
let mut command = self.new_command();
|
||||||
let managed = self.temp_dir.join("managed");
|
let managed = self.temp_dir.join("managed");
|
||||||
|
let bin = self.temp_dir.join("bin");
|
||||||
self.add_shared_args(&mut command, true);
|
self.add_shared_args(&mut command, true);
|
||||||
command
|
command
|
||||||
.arg("python")
|
.arg("python")
|
||||||
.arg("uninstall")
|
.arg("uninstall")
|
||||||
.env(EnvVars::UV_PYTHON_INSTALL_DIR, managed)
|
.env(EnvVars::UV_PYTHON_INSTALL_DIR, managed)
|
||||||
|
.env(EnvVars::UV_PYTHON_BIN_DIR, bin)
|
||||||
.current_dir(&self.temp_dir);
|
.current_dir(&self.temp_dir);
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
|
@ -411,13 +411,15 @@ fn help_subsubcommand() {
|
||||||
|
|
||||||
Multiple Python versions may be requested.
|
Multiple Python versions may be requested.
|
||||||
|
|
||||||
Supports CPython and PyPy.
|
Supports CPython and PyPy. CPython distributions are downloaded from the `python-build-standalone`
|
||||||
|
project. PyPy distributions are downloaded from `python.org`.
|
||||||
CPython distributions are downloaded from the `python-build-standalone` project.
|
|
||||||
|
|
||||||
Python versions are installed into the uv Python directory, which can be retrieved with `uv python
|
Python versions are installed into the uv Python directory, which can be retrieved with `uv python
|
||||||
dir`. A `python` executable is not made globally available, managed Python versions are only used in
|
dir`.
|
||||||
uv commands or in active virtual environments.
|
|
||||||
|
A `python` executable is not made globally available, managed Python versions are only used in uv
|
||||||
|
commands or in active virtual environments. There is experimental support for adding Python
|
||||||
|
executables to the `PATH` — use the `--preview` flag to enable this behavior.
|
||||||
|
|
||||||
See `uv help python` to view supported request formats.
|
See `uv help python` to view supported request formats.
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use assert_fs::{assert::PathAssert, prelude::PathChild};
|
||||||
|
use predicates::prelude::predicate;
|
||||||
|
|
||||||
use crate::common::{uv_snapshot, TestContext};
|
use crate::common::{uv_snapshot, TestContext};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -16,6 +21,14 @@ fn python_install() {
|
||||||
+ cpython-3.13.0-[PLATFORM]
|
+ cpython-3.13.0-[PLATFORM]
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
|
let bin_python = context
|
||||||
|
.temp_dir
|
||||||
|
.child("bin")
|
||||||
|
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
|
||||||
|
|
||||||
|
// The executable should not be installed in the bin directory (requires preview)
|
||||||
|
bin_python.assert(predicate::path::missing());
|
||||||
|
|
||||||
// Should be a no-op when already installed
|
// Should be a no-op when already installed
|
||||||
uv_snapshot!(context.filters(), context.python_install(), @r###"
|
uv_snapshot!(context.filters(), context.python_install(), @r###"
|
||||||
success: true
|
success: true
|
||||||
|
@ -39,6 +52,114 @@ fn python_install() {
|
||||||
Found existing installation for Python 3.13: cpython-3.13.0-[PLATFORM]
|
Found existing installation for Python 3.13: cpython-3.13.0-[PLATFORM]
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
|
// You can opt-in to a reinstall
|
||||||
|
uv_snapshot!(context.filters(), context.python_install().arg("--reinstall"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Searching for Python installations
|
||||||
|
Found: cpython-3.13.0-[PLATFORM]
|
||||||
|
Installed Python 3.13.0 in [TIME]
|
||||||
|
~ cpython-3.13.0-[PLATFORM]
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Uninstallation requires an argument
|
||||||
|
uv_snapshot!(context.filters(), context.python_uninstall(), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
error: the following required arguments were not provided:
|
||||||
|
<TARGETS>...
|
||||||
|
|
||||||
|
Usage: uv python uninstall <TARGETS>...
|
||||||
|
|
||||||
|
For more information, try '--help'.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
uv_snapshot!(context.filters(), context.python_uninstall().arg("3.13"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 2
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Searching for Python versions matching: Python 3.13
|
||||||
|
error: No such file or directory (os error 2)
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn python_install_preview() {
|
||||||
|
let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys();
|
||||||
|
|
||||||
|
// Install the latest version
|
||||||
|
uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Searching for Python installations
|
||||||
|
Installed Python 3.13.0 in [TIME]
|
||||||
|
+ cpython-3.13.0-[PLATFORM]
|
||||||
|
warning: `[TEMP_DIR]/bin` is not on your PATH. To use the installed Python executable, run `export PATH="[TEMP_DIR]/bin:$PATH"`.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let bin_python = context
|
||||||
|
.temp_dir
|
||||||
|
.child("bin")
|
||||||
|
.child(format!("python3.13{}", std::env::consts::EXE_SUFFIX));
|
||||||
|
|
||||||
|
// The executable should be installed in the bin directory
|
||||||
|
bin_python.assert(predicate::path::exists());
|
||||||
|
|
||||||
|
// On Unix, it should be a link
|
||||||
|
#[cfg(unix)]
|
||||||
|
bin_python.assert(predicate::path::is_symlink());
|
||||||
|
|
||||||
|
// The executable should "work"
|
||||||
|
uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str())
|
||||||
|
.arg("-c").arg("import subprocess; print('hello world')"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
hello world
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Should be a no-op when already installed
|
||||||
|
uv_snapshot!(context.filters(), context.python_install().arg("--preview"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Searching for Python installations
|
||||||
|
Found: cpython-3.13.0-[PLATFORM]
|
||||||
|
Python is already available. Use `uv python install <request>` to install a specific version.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// You can opt-in to a reinstall
|
||||||
|
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("--reinstall"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Searching for Python installations
|
||||||
|
Found: cpython-3.13.0-[PLATFORM]
|
||||||
|
Installed Python 3.13.0 in [TIME]
|
||||||
|
~ cpython-3.13.0-[PLATFORM]
|
||||||
|
warning: `[TEMP_DIR]/bin` is not on your PATH. To use the installed Python executable, run `export PATH="[TEMP_DIR]/bin:$PATH"`.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// The executable should still be present in the bin directory
|
||||||
|
bin_python.assert(predicate::path::exists());
|
||||||
|
|
||||||
// Uninstallation requires an argument
|
// Uninstallation requires an argument
|
||||||
uv_snapshot!(context.filters(), context.python_uninstall(), @r###"
|
uv_snapshot!(context.filters(), context.python_uninstall(), @r###"
|
||||||
success: false
|
success: false
|
||||||
|
@ -64,6 +185,9 @@ fn python_install() {
|
||||||
Uninstalled Python 3.13.0 in [TIME]
|
Uninstalled Python 3.13.0 in [TIME]
|
||||||
- cpython-3.13.0-[PLATFORM]
|
- cpython-3.13.0-[PLATFORM]
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
|
// The executable should be removed
|
||||||
|
bin_python.assert(predicate::path::missing());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -71,7 +195,7 @@ fn python_install_freethreaded() {
|
||||||
let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys();
|
let context: TestContext = TestContext::new_with_versions(&[]).with_filtered_python_keys();
|
||||||
|
|
||||||
// Install the latest version
|
// Install the latest version
|
||||||
uv_snapshot!(context.filters(), context.python_install().arg("3.13t"), @r###"
|
uv_snapshot!(context.filters(), context.python_install().arg("--preview").arg("3.13t"), @r###"
|
||||||
success: true
|
success: true
|
||||||
exit_code: 0
|
exit_code: 0
|
||||||
----- stdout -----
|
----- stdout -----
|
||||||
|
@ -80,6 +204,30 @@ fn python_install_freethreaded() {
|
||||||
Searching for Python versions matching: Python 3.13t
|
Searching for Python versions matching: Python 3.13t
|
||||||
Installed Python 3.13.0 in [TIME]
|
Installed Python 3.13.0 in [TIME]
|
||||||
+ cpython-3.13.0+freethreaded-[PLATFORM]
|
+ cpython-3.13.0+freethreaded-[PLATFORM]
|
||||||
|
warning: `[TEMP_DIR]/bin` is not on your PATH. To use the installed Python executable, run `export PATH="[TEMP_DIR]/bin:$PATH"`.
|
||||||
|
"###);
|
||||||
|
|
||||||
|
let bin_python = context
|
||||||
|
.temp_dir
|
||||||
|
.child("bin")
|
||||||
|
.child(format!("python3.13t{}", std::env::consts::EXE_SUFFIX));
|
||||||
|
|
||||||
|
// The executable should be installed in the bin directory
|
||||||
|
bin_python.assert(predicate::path::exists());
|
||||||
|
|
||||||
|
// On Unix, it should be a link
|
||||||
|
#[cfg(unix)]
|
||||||
|
bin_python.assert(predicate::path::is_symlink());
|
||||||
|
|
||||||
|
// The executable should "work"
|
||||||
|
uv_snapshot!(context.filters(), Command::new(bin_python.as_os_str())
|
||||||
|
.arg("-c").arg("import subprocess; print('hello world')"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
hello world
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
"###);
|
"###);
|
||||||
|
|
||||||
// Should be distinct from 3.13
|
// Should be distinct from 3.13
|
||||||
|
|
|
@ -4302,11 +4302,11 @@ Download and install Python versions.
|
||||||
|
|
||||||
Multiple Python versions may be requested.
|
Multiple Python versions may be requested.
|
||||||
|
|
||||||
Supports CPython and PyPy.
|
Supports CPython and PyPy. CPython distributions are downloaded from the `python-build-standalone` project. PyPy distributions are downloaded from `python.org`.
|
||||||
|
|
||||||
CPython distributions are downloaded from the `python-build-standalone` project.
|
Python versions are installed into the uv Python directory, which can be retrieved with `uv python dir`.
|
||||||
|
|
||||||
Python versions are installed into the uv Python directory, which can be retrieved with `uv python dir`. A `python` executable is not made globally available, managed Python versions are only used in uv commands or in active virtual environments.
|
A `python` executable is not made globally available, managed Python versions are only used in uv commands or in active virtual environments. There is experimental support for adding Python executables to the `PATH` — use the `--preview` flag to enable this behavior.
|
||||||
|
|
||||||
See `uv help python` to view supported request formats.
|
See `uv help python` to view supported request formats.
|
||||||
|
|
||||||
|
@ -4693,6 +4693,8 @@ By default, Python installations are stored in the uv data directory at `$XDG_DA
|
||||||
|
|
||||||
The Python installation directory may be overridden with `$UV_PYTHON_INSTALL_DIR`.
|
The Python installation directory may be overridden with `$UV_PYTHON_INSTALL_DIR`.
|
||||||
|
|
||||||
|
To instead view the directory uv installs Python executables into, use the `--bin` flag.
|
||||||
|
|
||||||
<h3 class="cli-reference">Usage</h3>
|
<h3 class="cli-reference">Usage</h3>
|
||||||
|
|
||||||
```
|
```
|
||||||
|
@ -4701,7 +4703,25 @@ uv python dir [OPTIONS]
|
||||||
|
|
||||||
<h3 class="cli-reference">Options</h3>
|
<h3 class="cli-reference">Options</h3>
|
||||||
|
|
||||||
<dl class="cli-reference"><dt><code>--cache-dir</code> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
|
<dl class="cli-reference"><dt><code>--bin</code></dt><dd><p>Show the directory into which <code>uv python</code> will install Python executables.</p>
|
||||||
|
|
||||||
|
<p>Note this directory is only used when installing with preview mode enabled.</p>
|
||||||
|
|
||||||
|
<p>By default, <code>uv python dir</code> shows the directory into which the Python distributions themselves are installed, rather than the directory containing the linked executables.</p>
|
||||||
|
|
||||||
|
<p>The Python executable directory is determined according to the XDG standard and is derived from the following environment variables, in order of preference:</p>
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><code>$UV_PYTHON_BIN_DIR</code></li>
|
||||||
|
|
||||||
|
<li><code>$XDG_BIN_HOME</code></li>
|
||||||
|
|
||||||
|
<li><code>$XDG_DATA_HOME/../bin</code></li>
|
||||||
|
|
||||||
|
<li><code>$HOME/.local/bin</code></li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</dd><dt><code>--cache-dir</code> <i>cache-dir</i></dt><dd><p>Path to the cache directory.</p>
|
||||||
|
|
||||||
<p>Defaults to <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on macOS and Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>
|
<p>Defaults to <code>$XDG_CACHE_HOME/uv</code> or <code>$HOME/.cache/uv</code> on macOS and Linux, and <code>%LOCALAPPDATA%\uv\cache</code> on Windows.</p>
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue