Add --install-dir arg to uv python install and uninstall (#7920)

## Summary

This PR adds `--install-dir` argument for the following commands:
- `uv python install`
- `uv python uninstall`

The `UV_PYTHON_INSTALL_DIR` env variable can be used to set it
(previously it was also used internally).

Any more commands we would want to add this to? 

## Test Plan

For now just manual test (works on my machine hehe)

```
❯ ./target/debug/uv python install --install-dir /tmp/pythons 3.8.12
Searching for Python versions matching: Python 3.8.12
Installed Python 3.8.12 in 4.31s
 + cpython-3.8.12-linux-x86_64-gnu
❯ /tmp/pythons/cpython-3.8.12-linux-x86_64-gnu/bin/python --help
usage: /tmp/pythons/cpython-3.8.12-linux-x86_64-gnu/bin/python [option] ... [-c cmd | -m mod | file | -] [arg] ...
```

Open to add some tests after the initial feedback.

---------

Co-authored-by: Zanie Blue <contact@zanie.dev>
This commit is contained in:
Daniel Gafni 2024-12-10 19:04:31 +02:00 committed by GitHub
parent b751648bfe
commit d0ccc9a16f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 76 additions and 19 deletions

View file

@ -4250,6 +4250,16 @@ pub struct PythonDirArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct PythonInstallArgs {
/// The directory to store the Python installation in.
///
/// If provided, `UV_PYTHON_INSTALL_DIR` will need to be set for subsequent operations for
/// uv to discover the Python installation.
///
/// See `uv python dir` to view the current Python installation directory. Defaults to
/// `~/.local/share/uv/python`.
#[arg(long, short, env = "UV_PYTHON_INSTALL_DIR")]
pub install_dir: Option<PathBuf>,
/// The Python version(s) to install.
///
/// If not provided, the requested Python version(s) will be read from the
@ -4310,6 +4320,10 @@ pub struct PythonInstallArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct PythonUninstallArgs {
/// The directory where the Python was installed.
#[arg(long, short, env = "UV_PYTHON_INSTALL_DIR")]
pub install_dir: Option<PathBuf>,
/// The Python version(s) to uninstall.
///
/// See `uv help python` to view supported request formats.

View file

@ -302,7 +302,7 @@ fn python_executables_from_installed<'a>(
preference: PythonPreference,
) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
let from_managed_installations = std::iter::once_with(move || {
ManagedPythonInstallations::from_settings()
ManagedPythonInstallations::from_settings(None)
.map_err(Error::from)
.and_then(|installed_installations| {
debug!(

View file

@ -135,7 +135,7 @@ impl PythonInstallation {
python_install_mirror: Option<&str>,
pypy_install_mirror: Option<&str>,
) -> Result<Self, Error> {
let installations = ManagedPythonInstallations::from_settings()?.init()?;
let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
let installations_dir = installations.root();
let scratch_dir = installations.scratch();
let _lock = installations.lock().await?;

View file

@ -107,11 +107,15 @@ impl ManagedPythonInstallations {
}
/// Prefer, in order:
/// 1. The specific Python directory specified by the user, i.e., `UV_PYTHON_INSTALL_DIR`
/// 2. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/python`
/// 3. A directory in the local data directory, e.g., `./.uv/python`
pub fn from_settings() -> Result<Self, Error> {
if let Some(install_dir) = std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR) {
///
/// 1. The specific Python directory passed via the `install_dir` argument.
/// 2. The specific Python directory specified with the `UV_PYTHON_INSTALL_DIR` environment variable.
/// 3. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/python`.
/// 4. A directory in the local data directory, e.g., `./.uv/python`.
pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
if let Some(install_dir) = install_dir {
Ok(Self::from_path(install_dir))
} else if let Some(install_dir) = std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR) {
Ok(Self::from_path(install_dir))
} else {
Ok(Self::from_path(
@ -227,7 +231,7 @@ impl ManagedPythonInstallations {
) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation>, Error> {
let platform_key = platform_key_from_env()?;
let iter = ManagedPythonInstallations::from_settings()?
let iter = ManagedPythonInstallations::from_settings(None)?
.find_all()?
.filter(move |installation| {
installation

View file

@ -11,7 +11,7 @@ pub(crate) fn dir(bin: bool) -> anyhow::Result<()> {
let bin = python_executable_dir()?;
println!("{}", bin.simplified_display().cyan());
} else {
let installed_toolchains = ManagedPythonInstallations::from_settings()
let installed_toolchains = ManagedPythonInstallations::from_settings(None)
.context("Failed to initialize toolchain settings")?;
println!(
"{}",

View file

@ -121,6 +121,7 @@ impl Changelog {
#[allow(clippy::fn_params_excessive_bools)]
pub(crate) async fn install(
project_dir: &Path,
install_dir: Option<PathBuf>,
targets: Vec<String>,
reinstall: bool,
force: bool,
@ -178,7 +179,7 @@ pub(crate) async fn install(
};
// Read the existing installations, lock the directory for the duration
let installations = ManagedPythonInstallations::from_settings()?.init()?;
let installations = ManagedPythonInstallations::from_settings(install_dir)?.init()?;
let installations_dir = installations.root();
let scratch_dir = installations.scratch();
let _lock = installations.lock().await?;

View file

@ -22,12 +22,14 @@ use crate::printer::Printer;
/// Uninstall managed Python versions.
pub(crate) async fn uninstall(
install_dir: Option<PathBuf>,
targets: Vec<String>,
all: bool,
printer: Printer,
) -> Result<ExitStatus> {
let installations = ManagedPythonInstallations::from_settings()?.init()?;
let installations = ManagedPythonInstallations::from_settings(install_dir)?.init()?;
let _lock = installations.lock().await?;
// Perform the uninstallation.

View file

@ -1104,6 +1104,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
commands::python_install(
&project_dir,
args.install_dir,
args.targets,
args.reinstall,
args.force,
@ -1127,7 +1128,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
let args = settings::PythonUninstallSettings::resolve(args, filesystem);
show_settings!(args);
commands::python_uninstall(args.targets, args.all, printer).await
commands::python_uninstall(args.install_dir, args.targets, args.all, printer).await
}
Commands::Python(PythonNamespace {
command: PythonCommand::Find(args),

View file

@ -760,6 +760,7 @@ impl PythonDirSettings {
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct PythonInstallSettings {
pub(crate) install_dir: Option<PathBuf>,
pub(crate) targets: Vec<String>,
pub(crate) reinstall: bool,
pub(crate) force: bool,
@ -784,6 +785,7 @@ impl PythonInstallSettings {
let pypy_mirror = args.pypy_mirror.or(pypy_mirror);
let PythonInstallArgs {
install_dir,
targets,
reinstall,
force,
@ -793,6 +795,7 @@ impl PythonInstallSettings {
} = args;
Self {
install_dir,
targets,
reinstall,
force,
@ -807,6 +810,7 @@ impl PythonInstallSettings {
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Clone)]
pub(crate) struct PythonUninstallSettings {
pub(crate) install_dir: Option<PathBuf>,
pub(crate) targets: Vec<String>,
pub(crate) all: bool,
}
@ -818,9 +822,17 @@ impl PythonUninstallSettings {
args: PythonUninstallArgs,
_filesystem: Option<FilesystemOptions>,
) -> Self {
let PythonUninstallArgs { targets, all } = args;
let PythonUninstallArgs {
install_dir,
targets,
all,
} = args;
Self { targets, all }
Self {
install_dir,
targets,
all,
}
}
}

View file

@ -1075,7 +1075,7 @@ pub fn venv_to_interpreter(venv: &Path) -> PathBuf {
/// Get the path to the python interpreter for a specific python version.
pub fn get_python(version: &PythonVersion) -> PathBuf {
ManagedPythonInstallations::from_settings()
ManagedPythonInstallations::from_settings(None)
.map(|installed_pythons| {
installed_pythons
.find_version(version)

View file

@ -450,7 +450,7 @@ fn help_subcommand() {
fn help_subsubcommand() {
let context = TestContext::new_with_versions(&[]);
uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r##"
uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r###"
success: true
exit_code: 0
----- stdout -----
@ -483,6 +483,17 @@ fn help_subsubcommand() {
See `uv help python` to view supported request formats.
Options:
-i, --install-dir <INSTALL_DIR>
The directory to store the Python installation in.
If provided, `UV_PYTHON_INSTALL_DIR` will need to be set for subsequent operations for uv
to discover the Python installation.
See `uv python dir` to view the current Python installation directory. Defaults to
`~/.local/share/uv/python`.
[env: UV_PYTHON_INSTALL_DIR=]
--mirror <MIRROR>
Set the URL to use as the source for downloading Python installations.
@ -673,7 +684,7 @@ fn help_subsubcommand() {
----- stderr -----
"##);
"###);
}
#[test]
@ -759,6 +770,8 @@ fn help_flag_subsubcommand() {
[TARGETS]... The Python version(s) to install
Options:
-i, --install-dir <INSTALL_DIR> The directory to store the Python installation in [env:
UV_PYTHON_INSTALL_DIR=]
--mirror <MIRROR> Set the URL to use as the source for downloading Python
installations [env: UV_PYTHON_INSTALL_MIRROR=]
--pypy-mirror <PYPY_MIRROR> Set the URL to use as the source for downloading PyPy

View file

@ -74,7 +74,7 @@ fn python_install() {
error: the following required arguments were not provided:
<TARGETS>...
Usage: uv python uninstall <TARGETS>...
Usage: uv python uninstall --install-dir <INSTALL_DIR> <TARGETS>...
For more information, try '--help'.
"###);
@ -209,7 +209,7 @@ fn python_install_preview() {
error: the following required arguments were not provided:
<TARGETS>...
Usage: uv python uninstall <TARGETS>...
Usage: uv python uninstall --install-dir <INSTALL_DIR> <TARGETS>...
For more information, try '--help'.
"###);

View file

@ -4553,6 +4553,13 @@ uv python install [OPTIONS] [TARGETS]...
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
</dd><dt><code>--install-dir</code>, <code>-i</code> <i>install-dir</i></dt><dd><p>The directory to store the Python installation in.</p>
<p>If provided, <code>UV_PYTHON_INSTALL_DIR</code> will need to be set for subsequent operations for uv to discover the Python installation.</p>
<p>See <code>uv python dir</code> to view the current Python installation directory. Defaults to <code>~/.local/share/uv/python</code>.</p>
<p>May also be set with the <code>UV_PYTHON_INSTALL_DIR</code> environment variable.</p>
</dd><dt><code>--mirror</code> <i>mirror</i></dt><dd><p>Set the URL to use as the source for downloading Python installations.</p>
<p>The provided URL will replace <code>https://github.com/indygreg/python-build-standalone/releases/download</code> in, e.g., <code>https://github.com/indygreg/python-build-standalone/releases/download/20240713/cpython-3.12.4%2B20240713-aarch64-apple-darwin-install_only.tar.gz</code>.</p>
@ -5114,6 +5121,9 @@ uv python uninstall [OPTIONS] <TARGETS>...
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
</dd><dt><code>--install-dir</code>, <code>-i</code> <i>install-dir</i></dt><dd><p>The directory where the Python was installed</p>
<p>May also be set with the <code>UV_PYTHON_INSTALL_DIR</code> environment variable.</p>
</dd><dt><code>--native-tls</code></dt><dd><p>Whether to load TLS certificates from the platform&#8217;s native certificate store.</p>
<p>By default, uv loads certificates from the bundled <code>webpki-roots</code> crate. The <code>webpki-roots</code> are a reliable set of trust roots from Mozilla, and including them in uv improves portability and performance (especially on macOS).</p>