Skip to content

Commit

Permalink
Add --install-dir arg to uv python install and uninstall (#7920)
Browse files Browse the repository at this point in the history
## 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 <[email protected]>
  • Loading branch information
danielgafni and zanieb authored Dec 10, 2024
1 parent b751648 commit d0ccc9a
Show file tree
Hide file tree
Showing 13 changed files with 76 additions and 19 deletions.
14 changes: 14 additions & 0 deletions crates/uv-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down
2 changes: 1 addition & 1 deletion crates/uv-python/src/installation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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?;
Expand Down
16 changes: 10 additions & 6 deletions crates/uv-python/src/managed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/python/dir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
"{}",
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/src/commands/python/install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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?;
Expand Down
4 changes: 3 additions & 1 deletion crates/uv/src/commands/python/uninstall.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
3 changes: 2 additions & 1 deletion crates/uv/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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),
Expand Down
16 changes: 14 additions & 2 deletions crates/uv/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -784,6 +785,7 @@ impl PythonInstallSettings {
let pypy_mirror = args.pypy_mirror.or(pypy_mirror);

let PythonInstallArgs {
install_dir,
targets,
reinstall,
force,
Expand All @@ -793,6 +795,7 @@ impl PythonInstallSettings {
} = args;

Self {
install_dir,
targets,
reinstall,
force,
Expand All @@ -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,
}
Expand All @@ -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,
}
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/uv/tests/it/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
17 changes: 15 additions & 2 deletions crates/uv/tests/it/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 -----
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -673,7 +684,7 @@ fn help_subsubcommand() {
----- stderr -----
"##);
"###);
}

#[test]
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions crates/uv/tests/it/python_install.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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'.
"###);
Expand Down Expand Up @@ -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'.
"###);
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand Down

0 comments on commit d0ccc9a

Please sign in to comment.