Skip to content

Commit df9927a

Browse files
author
fdfz
committed
Add --root flag for uv pip install
1 parent 222f988 commit df9927a

File tree

19 files changed

+579
-30
lines changed

19 files changed

+579
-30
lines changed

crates/uv-cli/src/lib.rs

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1946,7 +1946,7 @@ pub struct PipSyncArgs {
19461946
/// environment and only searches for a Python interpreter to use for package resolution.
19471947
/// If a suitable Python interpreter cannot be found, uv will install one.
19481948
/// To disable this, add `--no-python-downloads`.
1949-
#[arg(short = 't', long, conflicts_with = "prefix", value_hint = ValueHint::DirPath)]
1949+
#[arg(short = 't', long, conflicts_with_all = ["prefix", "root"], value_hint = ValueHint::DirPath)]
19501950
pub target: Option<PathBuf>,
19511951

19521952
/// Install packages into `lib`, `bin`, and other top-level folders under the specified
@@ -1961,9 +1961,18 @@ pub struct PipSyncArgs {
19611961
/// environment and only searches for a Python interpreter to use for package resolution.
19621962
/// If a suitable Python interpreter cannot be found, uv will install one.
19631963
/// To disable this, add `--no-python-downloads`.
1964-
#[arg(long, conflicts_with = "target", value_hint = ValueHint::DirPath)]
1964+
#[arg(long, conflicts_with_all = ["target", "root"], value_hint = ValueHint::DirPath)]
19651965
pub prefix: Option<PathBuf>,
19661966

1967+
/// Install everything relative to this alternate root directory.
1968+
///
1969+
/// Unlike other install operations, this command does not require discovery of an existing Python
1970+
/// environment and only searches for a Python interpreter to use for package resolution.
1971+
/// If a suitable Python interpreter cannot be found, uv will install one.
1972+
/// To disable this, add `--no-python-downloads`.
1973+
#[arg(long, conflicts_with_all = ["target", "prefix"], value_hint = ValueHint::DirPath)]
1974+
pub root: Option<PathBuf>,
1975+
19671976
/// Don't build source distributions.
19681977
///
19691978
/// When enabled, resolving will not run arbitrary Python code. The cached wheels of
@@ -2323,7 +2332,7 @@ pub struct PipInstallArgs {
23232332
/// environment and only searches for a Python interpreter to use for package resolution.
23242333
/// If a suitable Python interpreter cannot be found, uv will install one.
23252334
/// To disable this, add `--no-python-downloads`.
2326-
#[arg(short = 't', long, conflicts_with = "prefix", value_hint = ValueHint::DirPath)]
2335+
#[arg(short = 't', long, conflicts_with_all = ["prefix", "root"], value_hint = ValueHint::DirPath)]
23272336
pub target: Option<PathBuf>,
23282337

23292338
/// Install packages into `lib`, `bin`, and other top-level folders under the specified
@@ -2338,9 +2347,18 @@ pub struct PipInstallArgs {
23382347
/// environment and only searches for a Python interpreter to use for package resolution.
23392348
/// If a suitable Python interpreter cannot be found, uv will install one.
23402349
/// To disable this, add `--no-python-downloads`.
2341-
#[arg(long, conflicts_with = "target", value_hint = ValueHint::DirPath)]
2350+
#[arg(long, conflicts_with_all = ["target", "root"], value_hint = ValueHint::DirPath)]
23422351
pub prefix: Option<PathBuf>,
23432352

2353+
/// Install everything relative to this alternate root directory.
2354+
///
2355+
/// Unlike other install operations, this command does not require discovery of an existing Python
2356+
/// environment and only searches for a Python interpreter to use for package resolution.
2357+
/// If a suitable Python interpreter cannot be found, uv will install one.
2358+
/// To disable this, add `--no-python-downloads`.
2359+
#[arg(long, conflicts_with_all = ["target", "prefix"], value_hint = ValueHint::DirPath)]
2360+
pub root: Option<PathBuf>,
2361+
23442362
/// Don't build source distributions.
23452363
///
23462364
/// When enabled, resolving will not run arbitrary Python code. The cached wheels of
@@ -2539,13 +2557,17 @@ pub struct PipUninstallArgs {
25392557
pub no_break_system_packages: bool,
25402558

25412559
/// Uninstall packages from the specified `--target` directory.
2542-
#[arg(short = 't', long, conflicts_with = "prefix", value_hint = ValueHint::DirPath)]
2560+
#[arg(short = 't', long, conflicts_with_all = ["prefix", "root"], value_hint = ValueHint::DirPath)]
25432561
pub target: Option<PathBuf>,
25442562

25452563
/// Uninstall packages from the specified `--prefix` directory.
2546-
#[arg(long, conflicts_with = "target", value_hint = ValueHint::DirPath)]
2564+
#[arg(long, conflicts_with_all = ["target", "root"], value_hint = ValueHint::DirPath)]
25472565
pub prefix: Option<PathBuf>,
25482566

2567+
/// Uninstall packages from the specified `--root` directory.
2568+
#[arg(long, conflicts_with_all = ["target", "prefix"], value_hint = ValueHint::DirPath)]
2569+
pub root: Option<PathBuf>,
2570+
25492571
/// Perform a dry run, i.e., don't actually uninstall anything but print the resulting plan.
25502572
#[arg(long)]
25512573
pub dry_run: bool,
@@ -2610,13 +2632,17 @@ pub struct PipFreezeArgs {
26102632
pub no_system: bool,
26112633

26122634
/// List packages from the specified `--target` directory.
2613-
#[arg(short = 't', long, conflicts_with_all = ["prefix", "paths"], value_hint = ValueHint::DirPath)]
2635+
#[arg(short = 't', long, conflicts_with_all = ["prefix", "root", "paths"], value_hint = ValueHint::DirPath)]
26142636
pub target: Option<PathBuf>,
26152637

26162638
/// List packages from the specified `--prefix` directory.
2617-
#[arg(long, conflicts_with_all = ["target", "paths"], value_hint = ValueHint::DirPath)]
2639+
#[arg(long, conflicts_with_all = ["target", "root", "paths"], value_hint = ValueHint::DirPath)]
26182640
pub prefix: Option<PathBuf>,
26192641

2642+
/// List packages from the specified `--root` directory.
2643+
#[arg(long, conflicts_with_all = ["target", "prefix"], value_hint = ValueHint::DirPath)]
2644+
pub root: Option<PathBuf>,
2645+
26202646
#[command(flatten)]
26212647
pub compat_args: compat::PipGlobalCompatArgs,
26222648
}
@@ -2694,13 +2720,17 @@ pub struct PipListArgs {
26942720
pub no_system: bool,
26952721

26962722
/// List packages from the specified `--target` directory.
2697-
#[arg(short = 't', long, conflicts_with = "prefix", value_hint = ValueHint::DirPath)]
2723+
#[arg(short = 't', long, conflicts_with_all = ["prefix", "root"], value_hint = ValueHint::DirPath)]
26982724
pub target: Option<PathBuf>,
26992725

27002726
/// List packages from the specified `--prefix` directory.
2701-
#[arg(long, conflicts_with = "target", value_hint = ValueHint::DirPath)]
2727+
#[arg(long, conflicts_with_all = ["target", "root"], value_hint = ValueHint::DirPath)]
27022728
pub prefix: Option<PathBuf>,
27032729

2730+
/// List packages from the specified `--root` directory.
2731+
#[arg(long, conflicts_with_all = ["target", "prefix"], value_hint = ValueHint::DirPath)]
2732+
pub root: Option<PathBuf>,
2733+
27042734
#[command(flatten)]
27052735
pub compat_args: compat::PipListCompatArgs,
27062736
}
@@ -2820,13 +2850,17 @@ pub struct PipShowArgs {
28202850
pub no_system: bool,
28212851

28222852
/// Show a package from the specified `--target` directory.
2823-
#[arg(short = 't', long, conflicts_with = "prefix", value_hint = ValueHint::DirPath)]
2853+
#[arg(short = 't', long, conflicts_with_all = ["prefix", "root"], value_hint = ValueHint::DirPath)]
28242854
pub target: Option<PathBuf>,
28252855

28262856
/// Show a package from the specified `--prefix` directory.
2827-
#[arg(long, conflicts_with = "target", value_hint = ValueHint::DirPath)]
2857+
#[arg(long, conflicts_with_all = ["target", "root"], value_hint = ValueHint::DirPath)]
28282858
pub prefix: Option<PathBuf>,
28292859

2860+
/// Show a package from the specified `--root` directory.
2861+
#[arg(long, conflicts_with_all = ["target", "prefix"], value_hint = ValueHint::DirPath)]
2862+
pub root: Option<PathBuf>,
2863+
28302864
#[command(flatten)]
28312865
pub compat_args: compat::PipGlobalCompatArgs,
28322866
}

crates/uv-python/src/environment.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use uv_preview::Preview;
1313

1414
use crate::discovery::find_python_installation;
1515
use crate::installation::PythonInstallation;
16+
use crate::root::Root;
1617
use crate::virtualenv::{PyVenvConfiguration, virtualenv_python_executable};
1718
use crate::{
1819
EnvironmentPreference, Error, Interpreter, Prefix, PythonNotFound, PythonPreference,
@@ -257,6 +258,15 @@ impl PythonEnvironment {
257258
})))
258259
}
259260

261+
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--root` directory.
262+
pub fn with_root(self, root: Root) -> std::io::Result<Self> {
263+
let inner = Arc::unwrap_or_clone(self.0);
264+
Ok(Self(Arc::new(PythonEnvironmentShared {
265+
interpreter: inner.interpreter.with_root(root)?,
266+
..inner
267+
})))
268+
}
269+
260270
/// Returns the root (i.e., `prefix`) of the Python interpreter.
261271
pub fn root(&self) -> &Path {
262272
&self.0.root

crates/uv-python/src/interpreter.rs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ use crate::{
3636
VersionRequest, VirtualEnvironment,
3737
};
3838

39+
use crate::root::Root;
3940
#[cfg(windows)]
4041
use windows::Win32::Foundation::{APPMODEL_ERROR_NO_PACKAGE, ERROR_CANT_ACCESS_FILE, WIN32_ERROR};
4142

@@ -60,6 +61,7 @@ pub struct Interpreter {
6061
tags: OnceLock<Tags>,
6162
target: Option<Target>,
6263
prefix: Option<Prefix>,
64+
root: Option<Root>,
6365
pointer_size: PointerSize,
6466
gil_disabled: bool,
6567
real_executable: PathBuf,
@@ -98,6 +100,7 @@ impl Interpreter {
98100
tags: OnceLock::new(),
99101
target: None,
100102
prefix: None,
103+
root: None,
101104
real_executable: executable.as_ref().to_path_buf(),
102105
})
103106
}
@@ -135,6 +138,15 @@ impl Interpreter {
135138
})
136139
}
137140

141+
/// Return a new [`Interpreter`] to install into the given `--root` directory.
142+
pub fn with_root(self, root: Root) -> io::Result<Self> {
143+
root.init(&self)?;
144+
Ok(Self {
145+
root: Some(root),
146+
..self
147+
})
148+
}
149+
138150
/// Return the base Python executable; that is, the Python executable that should be
139151
/// considered the "base" for the virtual environment. This is typically the Python executable
140152
/// from the [`Interpreter`]; however, if the interpreter is a virtual environment itself, then
@@ -280,6 +292,11 @@ impl Interpreter {
280292
self.prefix.is_some()
281293
}
282294

295+
/// Returns `true` if the environment is a `--root` environment.
296+
pub fn is_root(&self) -> bool {
297+
self.root.is_some()
298+
}
299+
283300
/// Returns `true` if this interpreter is managed by uv.
284301
///
285302
/// Returns `false` if we cannot determine the path of the uv managed Python interpreters.
@@ -337,7 +354,7 @@ impl Interpreter {
337354
}
338355

339356
// If we're installing into a target or prefix directory, it's never externally managed.
340-
if self.is_target() || self.is_prefix() {
357+
if self.is_target() || self.is_prefix() || self.is_root() {
341358
return None;
342359
}
343360

@@ -555,6 +572,11 @@ impl Interpreter {
555572
self.prefix.as_ref()
556573
}
557574

575+
/// Return the `--root` directory for this interpreter, if any.
576+
pub fn root(&self) -> Option<&Root> {
577+
self.root.as_ref()
578+
}
579+
558580
/// Returns `true` if an [`Interpreter`] may be a `python-build-standalone` interpreter.
559581
///
560582
/// This method may return false positives, but it should not return false negatives. In other
@@ -586,6 +608,8 @@ impl Interpreter {
586608
target.scheme()
587609
} else if let Some(prefix) = self.prefix.as_ref() {
588610
prefix.scheme(&self.virtualenv)
611+
} else if let Some(root) = self.root.as_ref() {
612+
root.scheme(self)
589613
} else {
590614
Scheme {
591615
purelib: self.purelib().to_path_buf(),
@@ -625,7 +649,9 @@ impl Interpreter {
625649
.prefix()
626650
.map(|prefix| prefix.site_packages(self.virtualenv()));
627651

628-
let interpreter = if target.is_none() && prefix.is_none() {
652+
let root = self.root().map(|root| root.site_packages(self));
653+
654+
let interpreter = if target.is_none() && prefix.is_none() && root.is_none() {
629655
let purelib = self.purelib();
630656
let platlib = self.platlib();
631657
Some(std::iter::once(purelib).chain(
@@ -644,6 +670,7 @@ impl Interpreter {
644670
.flatten()
645671
.map(Cow::Borrowed)
646672
.chain(prefix.into_iter().flatten().map(Cow::Owned))
673+
.chain(root.into_iter().flatten().map(Cow::Owned))
647674
.chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
648675
}
649676

@@ -685,13 +712,21 @@ impl Interpreter {
685712
)
686713
.await
687714
} else if let Some(prefix) = self.prefix() {
688-
// Likewise, if we're installing into a `--prefix`, use a prefix-specific lockfile.
715+
// If we're installing into a `--prefix`, use a prefix-specific lockfile.
689716
LockedFile::acquire(
690717
prefix.root().join(".lock"),
691718
LockedFileMode::Exclusive,
692719
prefix.root().user_display(),
693720
)
694721
.await
722+
} else if let Some(root) = self.root() {
723+
// If we're installing into a `--root`, use a root-specific lockfile.
724+
LockedFile::acquire(
725+
root.root().join(".lock"),
726+
LockedFileMode::Exclusive,
727+
root.root().user_display(),
728+
)
729+
.await
695730
} else if self.is_virtualenv() {
696731
// If the environment a virtualenv, use a virtualenv-specific lockfile.
697732
LockedFile::acquire(

crates/uv-python/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub use crate::interpreter::{
2222
pub use crate::pointer_size::PointerSize;
2323
pub use crate::prefix::Prefix;
2424
pub use crate::python_version::{BuildVersionError, PythonVersion};
25+
pub use crate::root::Root;
2526
pub use crate::target::Target;
2627
pub use crate::version_files::{
2728
DiscoveryOptions as VersionFileDiscoveryOptions, FilePreference as VersionFilePreference,
@@ -42,6 +43,7 @@ mod microsoft_store;
4243
mod pointer_size;
4344
mod prefix;
4445
mod python_version;
46+
mod root;
4547
mod sysconfig;
4648
mod target;
4749
mod version_files;

0 commit comments

Comments
 (0)