Skip to content

Commit

Permalink
Add support for --prefix (#4085)
Browse files Browse the repository at this point in the history
## Summary

Closes #3076.
  • Loading branch information
charliermarsh authored Jun 6, 2024
1 parent 677a7f1 commit 52bdee2
Show file tree
Hide file tree
Showing 17 changed files with 280 additions and 43 deletions.
52 changes: 34 additions & 18 deletions crates/uv-interpreter/src/environment.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
use itertools::Either;
use std::borrow::Cow;
use std::env;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use uv_configuration::PreviewMode;

use same_file::is_same_file;

use uv_cache::Cache;
use uv_configuration::PreviewMode;
use uv_fs::{LockedFile, Simplified};

use crate::discovery::{InterpreterRequest, SourceSelector, SystemPython};
use crate::virtualenv::{virtualenv_python_executable, PyVenvConfiguration};
use crate::{
find_default_interpreter, find_interpreter, Error, Interpreter, InterpreterSource, Target,
find_default_interpreter, find_interpreter, Error, Interpreter, InterpreterSource, Prefix,
Target,
};

/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
Expand Down Expand Up @@ -159,6 +158,16 @@ impl PythonEnvironment {
}))
}

/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--prefix` directory.
#[must_use]
pub fn with_prefix(self, prefix: Prefix) -> Self {
let inner = Arc::unwrap_or_clone(self.0);
Self(Arc::new(PythonEnvironmentShared {
interpreter: inner.interpreter.with_prefix(prefix),
..inner
}))
}

/// Returns the root (i.e., `prefix`) of the Python interpreter.
pub fn root(&self) -> &Path {
&self.0.root
Expand Down Expand Up @@ -189,20 +198,27 @@ impl PythonEnvironment {
///
/// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
/// still deduplicate the entries, returning a single path.
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
if let Some(target) = self.0.interpreter.target() {
Either::Left(std::iter::once(target.root()))
pub fn site_packages(&self) -> impl Iterator<Item = Cow<Path>> {
let target = self.0.interpreter.target().map(Target::site_packages);

let prefix = self
.0
.interpreter
.prefix()
.map(|prefix| prefix.site_packages(self.0.interpreter.virtualenv()));

let interpreter = if target.is_none() && prefix.is_none() {
Some(self.0.interpreter.site_packages())
} else {
let purelib = self.0.interpreter.purelib();
let platlib = self.0.interpreter.platlib();
Either::Right(std::iter::once(purelib).chain(
if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
None
} else {
Some(platlib)
},
))
}
None
};

target
.into_iter()
.flatten()
.map(Cow::Borrowed)
.chain(prefix.into_iter().flatten().map(Cow::Owned))
.chain(interpreter.into_iter().flatten().map(Cow::Borrowed))
}

/// Returns the path to the `bin` directory inside this environment.
Expand Down
47 changes: 42 additions & 5 deletions crates/uv-interpreter/src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use std::process::{Command, ExitStatus};
use configparser::ini::Ini;
use fs_err as fs;
use once_cell::sync::OnceCell;
use same_file::is_same_file;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tracing::{trace, warn};
Expand All @@ -20,7 +21,7 @@ use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp};
use uv_fs::{write_atomic_sync, PythonExt, Simplified};

use crate::pointer_size::PointerSize;
use crate::{PythonVersion, Target, VirtualEnvironment};
use crate::{Prefix, PythonVersion, Target, VirtualEnvironment};

/// A Python executable and its associated platform markers.
#[derive(Debug, Clone)]
Expand All @@ -38,6 +39,7 @@ pub struct Interpreter {
stdlib: PathBuf,
tags: OnceCell<Tags>,
target: Option<Target>,
prefix: Option<Prefix>,
pointer_size: PointerSize,
gil_disabled: bool,
}
Expand Down Expand Up @@ -69,6 +71,7 @@ impl Interpreter {
stdlib: info.stdlib,
tags: OnceCell::new(),
target: None,
prefix: None,
})
}

Expand Down Expand Up @@ -100,6 +103,7 @@ impl Interpreter {
stdlib: PathBuf::from("/dev/null"),
tags: OnceCell::new(),
target: None,
prefix: None,
pointer_size: PointerSize::_64,
gil_disabled: false,
}
Expand All @@ -113,13 +117,12 @@ impl Interpreter {
sys_executable: virtualenv.executable,
sys_prefix: virtualenv.root,
target: None,
prefix: None,
..self
}
}

/// Return a new [`Interpreter`] to install into the given `--target` directory.
///
/// Initializes the `--target` directory with the expected layout.
#[must_use]
pub fn with_target(self, target: Target) -> Self {
Self {
Expand All @@ -128,6 +131,15 @@ impl Interpreter {
}
}

/// Return a new [`Interpreter`] to install into the given `--prefix` directory.
#[must_use]
pub fn with_prefix(self, prefix: Prefix) -> Self {
Self {
prefix: Some(prefix),
..self
}
}

/// Returns the path to the Python virtual environment.
#[inline]
pub fn platform(&self) -> &Platform {
Expand Down Expand Up @@ -166,6 +178,11 @@ impl Interpreter {
self.target.is_some()
}

/// Returns `true` if the environment is a `--prefix` environment.
pub fn is_prefix(&self) -> bool {
self.prefix.is_some()
}

/// Returns `Some` if the environment is externally managed, optionally including an error
/// message from the `EXTERNALLY-MANAGED` file.
///
Expand All @@ -176,8 +193,8 @@ impl Interpreter {
return None;
}

// If we're installing into a target directory, it's never externally managed.
if self.is_target() {
// If we're installing into a target or prefix directory, it's never externally managed.
if self.is_target() || self.is_prefix() {
return None;
}

Expand Down Expand Up @@ -357,6 +374,11 @@ impl Interpreter {
self.target.as_ref()
}

/// Return the `--prefix` directory for this interpreter, if any.
pub fn prefix(&self) -> Option<&Prefix> {
self.prefix.as_ref()
}

/// Return the [`Layout`] environment used to install wheels into this interpreter.
pub fn layout(&self) -> Layout {
Layout {
Expand All @@ -365,6 +387,8 @@ impl Interpreter {
os_name: self.markers.os_name().to_string(),
scheme: if let Some(target) = self.target.as_ref() {
target.scheme()
} else if let Some(prefix) = self.prefix.as_ref() {
prefix.scheme(&self.virtualenv)
} else {
Scheme {
purelib: self.purelib().to_path_buf(),
Expand All @@ -387,6 +411,19 @@ impl Interpreter {
}
}

/// Return an iterator over the `site-packages` directories inside the environment.
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
let purelib = self.purelib();
let platlib = self.platlib();
std::iter::once(purelib).chain(
if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
None
} else {
Some(platlib)
},
)
}

/// Check if the interpreter matches the given Python version.
///
/// If a patch version is present, we will require an exact match.
Expand Down
2 changes: 2 additions & 0 deletions crates/uv-interpreter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub use crate::discovery::{
pub use crate::environment::PythonEnvironment;
pub use crate::interpreter::Interpreter;
pub use crate::pointer_size::PointerSize;
pub use crate::prefix::Prefix;
pub use crate::python_version::PythonVersion;
pub use crate::target::Target;
pub use crate::virtualenv::{Error as VirtualEnvError, PyVenvConfiguration, VirtualEnvironment};
Expand All @@ -21,6 +22,7 @@ mod interpreter;
pub mod managed;
pub mod platform;
mod pointer_size;
mod prefix;
mod py_launcher;
mod python_version;
mod target;
Expand Down
43 changes: 43 additions & 0 deletions crates/uv-interpreter/src/prefix.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use std::path::{Path, PathBuf};

use pypi_types::Scheme;

/// A `--prefix` directory into which packages can be installed, separate from a virtual environment
/// or system Python interpreter.
#[derive(Debug, Clone)]
pub struct Prefix(PathBuf);

impl Prefix {
/// Return the [`Scheme`] for the `--prefix` directory.
pub fn scheme(&self, virtualenv: &Scheme) -> Scheme {
Scheme {
purelib: self.0.join(&virtualenv.purelib),
platlib: self.0.join(&virtualenv.platlib),
scripts: self.0.join(&virtualenv.scripts),
data: self.0.join(&virtualenv.data),
include: self.0.join(&virtualenv.include),
}
}

/// Return an iterator over the `site-packages` directories inside the environment.
pub fn site_packages(&self, virtualenv: &Scheme) -> impl Iterator<Item = PathBuf> {
std::iter::once(self.0.join(&virtualenv.purelib))
}

/// Initialize the `--prefix` directory.
pub fn init(&self) -> std::io::Result<()> {
fs_err::create_dir_all(&self.0)?;
Ok(())
}

/// Return the path to the `--prefix` directory.
pub fn root(&self) -> &Path {
&self.0
}
}

impl From<PathBuf> for Prefix {
fn from(path: PathBuf) -> Self {
Self(path)
}
}
5 changes: 5 additions & 0 deletions crates/uv-interpreter/src/target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ impl Target {
}
}

/// Return an iterator over the `site-packages` directories inside the environment.
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
std::iter::once(self.0.as_path())
}

/// Initialize the `--target` directory.
pub fn init(&self) -> std::io::Result<()> {
fs_err::create_dir_all(&self.0)?;
Expand Down
1 change: 1 addition & 0 deletions crates/uv-workspace/src/combine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ impl Combine for PipOptions {
.break_system_packages
.combine(other.break_system_packages),
target: self.target.combine(other.target),
prefix: self.prefix.combine(other.prefix),
index_url: self.index_url.combine(other.index_url),
extra_index_url: self.extra_index_url.combine(other.extra_index_url),
no_index: self.no_index.combine(other.no_index),
Expand Down
1 change: 1 addition & 0 deletions crates/uv-workspace/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ pub struct PipOptions {
pub system: Option<bool>,
pub break_system_packages: Option<bool>,
pub target: Option<PathBuf>,
pub prefix: Option<PathBuf>,
pub index_url: Option<IndexUrl>,
pub extra_index_url: Option<Vec<IndexUrl>>,
pub no_index: Option<bool>,
Expand Down
39 changes: 32 additions & 7 deletions crates/uv/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -731,10 +731,21 @@ pub(crate) struct PipSyncArgs {
pub(crate) no_break_system_packages: bool,

/// Install packages into the specified directory, rather than into the virtual environment
/// or system Python interpreter.
#[arg(long)]
/// or system Python interpreter. The packages will be installed at the top-level of the
/// directory
#[arg(long, conflicts_with = "prefix")]
pub(crate) target: Option<PathBuf>,

/// Install packages into `lib`, `bin`, and other top-level folders under the specified
/// directory, as if a virtual environment were created at the specified location.
///
/// In general, prefer the use of `--python` to install into an alternate environment, as
/// scripts and other artifacts installed via `--prefix` will reference the installing
/// interpreter, rather than any interpreter added to the `--prefix` directory, rendering them
/// non-portable.
#[arg(long, conflicts_with = "target")]
pub(crate) prefix: Option<PathBuf>,

/// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`.
#[arg(long, overrides_with("no_legacy_setup_py"))]
Expand Down Expand Up @@ -1087,10 +1098,21 @@ pub(crate) struct PipInstallArgs {
pub(crate) no_break_system_packages: bool,

/// Install packages into the specified directory, rather than into the virtual environment
/// or system Python interpreter.
#[arg(long)]
/// or system Python interpreter. The packages will be installed at the top-level of the
/// directory
#[arg(long, conflicts_with = "prefix")]
pub(crate) target: Option<PathBuf>,

/// Install packages into `lib`, `bin`, and other top-level folders under the specified
/// directory, as if a virtual environment were created at the specified location.
///
/// In general, prefer the use of `--python` to install into an alternate environment, as
/// scripts and other artifacts installed via `--prefix` will reference the installing
/// interpreter, rather than any interpreter added to the `--prefix` directory, rendering them
/// non-portable.
#[arg(long, conflicts_with = "target")]
pub(crate) prefix: Option<PathBuf>,

/// Use legacy `setuptools` behavior when building source distributions without a
/// `pyproject.toml`.
#[arg(long, overrides_with("no_legacy_setup_py"))]
Expand Down Expand Up @@ -1300,10 +1322,13 @@ pub(crate) struct PipUninstallArgs {
#[arg(long, overrides_with("break_system_packages"))]
pub(crate) no_break_system_packages: bool,

/// Uninstall packages from the specified directory, rather than from the virtual environment
/// or system Python interpreter.
#[arg(long)]
/// Uninstall packages from the specified `--target` directory.
#[arg(long, conflicts_with = "prefix")]
pub(crate) target: Option<PathBuf>,

/// Uninstall packages from the specified `--prefix` directory.
#[arg(long, conflicts_with = "target")]
pub(crate) prefix: Option<PathBuf>,
}

#[derive(Args)]
Expand Down
2 changes: 1 addition & 1 deletion crates/uv/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ pub(super) async fn compile_bytecode(
let start = std::time::Instant::now();
let mut files = 0;
for site_packages in venv.site_packages() {
files += compile_tree(site_packages, venv.python_executable(), cache.root())
files += compile_tree(&site_packages, venv.python_executable(), cache.root())
.await
.with_context(|| {
format!(
Expand Down
Loading

0 comments on commit 52bdee2

Please sign in to comment.