Skip to content

Commit d0ccc9a

Browse files
danielgafnizanieb
andauthored
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 <[email protected]>
1 parent b751648 commit d0ccc9a

File tree

13 files changed

+76
-19
lines changed

13 files changed

+76
-19
lines changed

crates/uv-cli/src/lib.rs

+14
Original file line numberDiff line numberDiff line change
@@ -4250,6 +4250,16 @@ pub struct PythonDirArgs {
42504250
#[derive(Args)]
42514251
#[allow(clippy::struct_excessive_bools)]
42524252
pub struct PythonInstallArgs {
4253+
/// The directory to store the Python installation in.
4254+
///
4255+
/// If provided, `UV_PYTHON_INSTALL_DIR` will need to be set for subsequent operations for
4256+
/// uv to discover the Python installation.
4257+
///
4258+
/// See `uv python dir` to view the current Python installation directory. Defaults to
4259+
/// `~/.local/share/uv/python`.
4260+
#[arg(long, short, env = "UV_PYTHON_INSTALL_DIR")]
4261+
pub install_dir: Option<PathBuf>,
4262+
42534263
/// The Python version(s) to install.
42544264
///
42554265
/// If not provided, the requested Python version(s) will be read from the
@@ -4310,6 +4320,10 @@ pub struct PythonInstallArgs {
43104320
#[derive(Args)]
43114321
#[allow(clippy::struct_excessive_bools)]
43124322
pub struct PythonUninstallArgs {
4323+
/// The directory where the Python was installed.
4324+
#[arg(long, short, env = "UV_PYTHON_INSTALL_DIR")]
4325+
pub install_dir: Option<PathBuf>,
4326+
43134327
/// The Python version(s) to uninstall.
43144328
///
43154329
/// See `uv help python` to view supported request formats.

crates/uv-python/src/discovery.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ fn python_executables_from_installed<'a>(
302302
preference: PythonPreference,
303303
) -> Box<dyn Iterator<Item = Result<(PythonSource, PathBuf), Error>> + 'a> {
304304
let from_managed_installations = std::iter::once_with(move || {
305-
ManagedPythonInstallations::from_settings()
305+
ManagedPythonInstallations::from_settings(None)
306306
.map_err(Error::from)
307307
.and_then(|installed_installations| {
308308
debug!(

crates/uv-python/src/installation.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ impl PythonInstallation {
135135
python_install_mirror: Option<&str>,
136136
pypy_install_mirror: Option<&str>,
137137
) -> Result<Self, Error> {
138-
let installations = ManagedPythonInstallations::from_settings()?.init()?;
138+
let installations = ManagedPythonInstallations::from_settings(None)?.init()?;
139139
let installations_dir = installations.root();
140140
let scratch_dir = installations.scratch();
141141
let _lock = installations.lock().await?;

crates/uv-python/src/managed.rs

+10-6
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,15 @@ impl ManagedPythonInstallations {
107107
}
108108

109109
/// Prefer, in order:
110-
/// 1. The specific Python directory specified by the user, i.e., `UV_PYTHON_INSTALL_DIR`
111-
/// 2. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/python`
112-
/// 3. A directory in the local data directory, e.g., `./.uv/python`
113-
pub fn from_settings() -> Result<Self, Error> {
114-
if let Some(install_dir) = std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR) {
110+
///
111+
/// 1. The specific Python directory passed via the `install_dir` argument.
112+
/// 2. The specific Python directory specified with the `UV_PYTHON_INSTALL_DIR` environment variable.
113+
/// 3. A directory in the system-appropriate user-level data directory, e.g., `~/.local/uv/python`.
114+
/// 4. A directory in the local data directory, e.g., `./.uv/python`.
115+
pub fn from_settings(install_dir: Option<PathBuf>) -> Result<Self, Error> {
116+
if let Some(install_dir) = install_dir {
117+
Ok(Self::from_path(install_dir))
118+
} else if let Some(install_dir) = std::env::var_os(EnvVars::UV_PYTHON_INSTALL_DIR) {
115119
Ok(Self::from_path(install_dir))
116120
} else {
117121
Ok(Self::from_path(
@@ -227,7 +231,7 @@ impl ManagedPythonInstallations {
227231
) -> Result<impl DoubleEndedIterator<Item = ManagedPythonInstallation>, Error> {
228232
let platform_key = platform_key_from_env()?;
229233

230-
let iter = ManagedPythonInstallations::from_settings()?
234+
let iter = ManagedPythonInstallations::from_settings(None)?
231235
.find_all()?
232236
.filter(move |installation| {
233237
installation

crates/uv/src/commands/python/dir.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub(crate) fn dir(bin: bool) -> anyhow::Result<()> {
1111
let bin = python_executable_dir()?;
1212
println!("{}", bin.simplified_display().cyan());
1313
} else {
14-
let installed_toolchains = ManagedPythonInstallations::from_settings()
14+
let installed_toolchains = ManagedPythonInstallations::from_settings(None)
1515
.context("Failed to initialize toolchain settings")?;
1616
println!(
1717
"{}",

crates/uv/src/commands/python/install.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ impl Changelog {
121121
#[allow(clippy::fn_params_excessive_bools)]
122122
pub(crate) async fn install(
123123
project_dir: &Path,
124+
install_dir: Option<PathBuf>,
124125
targets: Vec<String>,
125126
reinstall: bool,
126127
force: bool,
@@ -178,7 +179,7 @@ pub(crate) async fn install(
178179
};
179180

180181
// Read the existing installations, lock the directory for the duration
181-
let installations = ManagedPythonInstallations::from_settings()?.init()?;
182+
let installations = ManagedPythonInstallations::from_settings(install_dir)?.init()?;
182183
let installations_dir = installations.root();
183184
let scratch_dir = installations.scratch();
184185
let _lock = installations.lock().await?;

crates/uv/src/commands/python/uninstall.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,14 @@ use crate::printer::Printer;
2222

2323
/// Uninstall managed Python versions.
2424
pub(crate) async fn uninstall(
25+
install_dir: Option<PathBuf>,
2526
targets: Vec<String>,
2627
all: bool,
2728

2829
printer: Printer,
2930
) -> Result<ExitStatus> {
30-
let installations = ManagedPythonInstallations::from_settings()?.init()?;
31+
let installations = ManagedPythonInstallations::from_settings(install_dir)?.init()?;
32+
3133
let _lock = installations.lock().await?;
3234

3335
// Perform the uninstallation.

crates/uv/src/lib.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -1104,6 +1104,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
11041104

11051105
commands::python_install(
11061106
&project_dir,
1107+
args.install_dir,
11071108
args.targets,
11081109
args.reinstall,
11091110
args.force,
@@ -1127,7 +1128,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
11271128
let args = settings::PythonUninstallSettings::resolve(args, filesystem);
11281129
show_settings!(args);
11291130

1130-
commands::python_uninstall(args.targets, args.all, printer).await
1131+
commands::python_uninstall(args.install_dir, args.targets, args.all, printer).await
11311132
}
11321133
Commands::Python(PythonNamespace {
11331134
command: PythonCommand::Find(args),

crates/uv/src/settings.rs

+14-2
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,7 @@ impl PythonDirSettings {
760760
#[allow(clippy::struct_excessive_bools)]
761761
#[derive(Debug, Clone)]
762762
pub(crate) struct PythonInstallSettings {
763+
pub(crate) install_dir: Option<PathBuf>,
763764
pub(crate) targets: Vec<String>,
764765
pub(crate) reinstall: bool,
765766
pub(crate) force: bool,
@@ -784,6 +785,7 @@ impl PythonInstallSettings {
784785
let pypy_mirror = args.pypy_mirror.or(pypy_mirror);
785786

786787
let PythonInstallArgs {
788+
install_dir,
787789
targets,
788790
reinstall,
789791
force,
@@ -793,6 +795,7 @@ impl PythonInstallSettings {
793795
} = args;
794796

795797
Self {
798+
install_dir,
796799
targets,
797800
reinstall,
798801
force,
@@ -807,6 +810,7 @@ impl PythonInstallSettings {
807810
#[allow(clippy::struct_excessive_bools)]
808811
#[derive(Debug, Clone)]
809812
pub(crate) struct PythonUninstallSettings {
813+
pub(crate) install_dir: Option<PathBuf>,
810814
pub(crate) targets: Vec<String>,
811815
pub(crate) all: bool,
812816
}
@@ -818,9 +822,17 @@ impl PythonUninstallSettings {
818822
args: PythonUninstallArgs,
819823
_filesystem: Option<FilesystemOptions>,
820824
) -> Self {
821-
let PythonUninstallArgs { targets, all } = args;
825+
let PythonUninstallArgs {
826+
install_dir,
827+
targets,
828+
all,
829+
} = args;
822830

823-
Self { targets, all }
831+
Self {
832+
install_dir,
833+
targets,
834+
all,
835+
}
824836
}
825837
}
826838

crates/uv/tests/it/common/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1075,7 +1075,7 @@ pub fn venv_to_interpreter(venv: &Path) -> PathBuf {
10751075

10761076
/// Get the path to the python interpreter for a specific python version.
10771077
pub fn get_python(version: &PythonVersion) -> PathBuf {
1078-
ManagedPythonInstallations::from_settings()
1078+
ManagedPythonInstallations::from_settings(None)
10791079
.map(|installed_pythons| {
10801080
installed_pythons
10811081
.find_version(version)

crates/uv/tests/it/help.rs

+15-2
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ fn help_subcommand() {
450450
fn help_subsubcommand() {
451451
let context = TestContext::new_with_versions(&[]);
452452

453-
uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r##"
453+
uv_snapshot!(context.filters(), context.help().arg("python").arg("install"), @r###"
454454
success: true
455455
exit_code: 0
456456
----- stdout -----
@@ -483,6 +483,17 @@ fn help_subsubcommand() {
483483
See `uv help python` to view supported request formats.
484484
485485
Options:
486+
-i, --install-dir <INSTALL_DIR>
487+
The directory to store the Python installation in.
488+
489+
If provided, `UV_PYTHON_INSTALL_DIR` will need to be set for subsequent operations for uv
490+
to discover the Python installation.
491+
492+
See `uv python dir` to view the current Python installation directory. Defaults to
493+
`~/.local/share/uv/python`.
494+
495+
[env: UV_PYTHON_INSTALL_DIR=]
496+
486497
--mirror <MIRROR>
487498
Set the URL to use as the source for downloading Python installations.
488499
@@ -673,7 +684,7 @@ fn help_subsubcommand() {
673684
674685
675686
----- stderr -----
676-
"##);
687+
"###);
677688
}
678689

679690
#[test]
@@ -759,6 +770,8 @@ fn help_flag_subsubcommand() {
759770
[TARGETS]... The Python version(s) to install
760771
761772
Options:
773+
-i, --install-dir <INSTALL_DIR> The directory to store the Python installation in [env:
774+
UV_PYTHON_INSTALL_DIR=]
762775
--mirror <MIRROR> Set the URL to use as the source for downloading Python
763776
installations [env: UV_PYTHON_INSTALL_MIRROR=]
764777
--pypy-mirror <PYPY_MIRROR> Set the URL to use as the source for downloading PyPy

crates/uv/tests/it/python_install.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ fn python_install() {
7474
error: the following required arguments were not provided:
7575
<TARGETS>...
7676
77-
Usage: uv python uninstall <TARGETS>...
77+
Usage: uv python uninstall --install-dir <INSTALL_DIR> <TARGETS>...
7878
7979
For more information, try '--help'.
8080
"###);
@@ -209,7 +209,7 @@ fn python_install_preview() {
209209
error: the following required arguments were not provided:
210210
<TARGETS>...
211211
212-
Usage: uv python uninstall <TARGETS>...
212+
Usage: uv python uninstall --install-dir <INSTALL_DIR> <TARGETS>...
213213
214214
For more information, try '--help'.
215215
"###);

docs/reference/cli.md

+10
Original file line numberDiff line numberDiff line change
@@ -4553,6 +4553,13 @@ uv python install [OPTIONS] [TARGETS]...
45534553

45544554
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
45554555

4556+
</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>
4557+
4558+
<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>
4559+
4560+
<p>See <code>uv python dir</code> to view the current Python installation directory. Defaults to <code>~/.local/share/uv/python</code>.</p>
4561+
4562+
<p>May also be set with the <code>UV_PYTHON_INSTALL_DIR</code> environment variable.</p>
45564563
</dd><dt><code>--mirror</code> <i>mirror</i></dt><dd><p>Set the URL to use as the source for downloading Python installations.</p>
45574564

45584565
<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>...
51145121

51155122
</dd><dt><code>--help</code>, <code>-h</code></dt><dd><p>Display the concise help for this command</p>
51165123

5124+
</dd><dt><code>--install-dir</code>, <code>-i</code> <i>install-dir</i></dt><dd><p>The directory where the Python was installed</p>
5125+
5126+
<p>May also be set with the <code>UV_PYTHON_INSTALL_DIR</code> environment variable.</p>
51175127
</dd><dt><code>--native-tls</code></dt><dd><p>Whether to load TLS certificates from the platform&#8217;s native certificate store.</p>
51185128

51195129
<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>

0 commit comments

Comments
 (0)