mirror of
https://github.com/astral-sh/uv.git
synced 2025-11-01 12:24:15 +00:00
Add --target support to sync and install (#3257)
## Summary The approach taken here is to model `--target` as an install scheme in which all the directories are just subdirectories of the `--target`. From there, everything else... just works? Like, upgrade, uninstalls, editables, etc. all "just work". Closes #1517.
This commit is contained in:
parent
71ffb2eabc
commit
ed8f6e4556
14 changed files with 284 additions and 36 deletions
|
|
@ -26,6 +26,7 @@ uv-warnings = { workspace = true }
|
||||||
|
|
||||||
configparser = { workspace = true }
|
configparser = { workspace = true }
|
||||||
fs-err = { workspace = true, features = ["tokio"] }
|
fs-err = { workspace = true, features = ["tokio"] }
|
||||||
|
itertools = { workspace = true }
|
||||||
once_cell = { workspace = true }
|
once_cell = { workspace = true }
|
||||||
regex = { workspace = true }
|
regex = { workspace = true }
|
||||||
rmp-serde = { workspace = true }
|
rmp-serde = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,8 @@ use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp};
|
||||||
use uv_fs::{write_atomic_sync, PythonExt, Simplified};
|
use uv_fs::{write_atomic_sync, PythonExt, Simplified};
|
||||||
use uv_toolchain::PythonVersion;
|
use uv_toolchain::PythonVersion;
|
||||||
|
|
||||||
use crate::Error;
|
|
||||||
use crate::Virtualenv;
|
use crate::Virtualenv;
|
||||||
|
use crate::{Error, Target};
|
||||||
|
|
||||||
/// A Python executable and its associated platform markers.
|
/// A Python executable and its associated platform markers.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -35,6 +35,7 @@ pub struct Interpreter {
|
||||||
sys_executable: PathBuf,
|
sys_executable: PathBuf,
|
||||||
stdlib: PathBuf,
|
stdlib: PathBuf,
|
||||||
tags: OnceCell<Tags>,
|
tags: OnceCell<Tags>,
|
||||||
|
target: Option<Target>,
|
||||||
gil_disabled: bool,
|
gil_disabled: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -62,6 +63,7 @@ impl Interpreter {
|
||||||
sys_executable: info.sys_executable,
|
sys_executable: info.sys_executable,
|
||||||
stdlib: info.stdlib,
|
stdlib: info.stdlib,
|
||||||
tags: OnceCell::new(),
|
tags: OnceCell::new(),
|
||||||
|
target: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,6 +93,7 @@ impl Interpreter {
|
||||||
sys_executable: PathBuf::from("/dev/null"),
|
sys_executable: PathBuf::from("/dev/null"),
|
||||||
stdlib: PathBuf::from("/dev/null"),
|
stdlib: PathBuf::from("/dev/null"),
|
||||||
tags: OnceCell::new(),
|
tags: OnceCell::new(),
|
||||||
|
target: None,
|
||||||
gil_disabled: false,
|
gil_disabled: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -106,6 +109,17 @@ impl Interpreter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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 {
|
||||||
|
target: Some(target),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the path to the Python virtual environment.
|
/// Returns the path to the Python virtual environment.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn platform(&self) -> &Platform {
|
pub fn platform(&self) -> &Platform {
|
||||||
|
|
@ -135,9 +149,15 @@ impl Interpreter {
|
||||||
///
|
///
|
||||||
/// See: <https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/utils/virtualenv.py#L14>
|
/// See: <https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/utils/virtualenv.py#L14>
|
||||||
pub fn is_virtualenv(&self) -> bool {
|
pub fn is_virtualenv(&self) -> bool {
|
||||||
|
// Maybe this should return `false` if it's a target?
|
||||||
self.prefix != self.base_prefix
|
self.prefix != self.base_prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns `true` if the environment is a `--target` environment.
|
||||||
|
pub fn is_target(&self) -> bool {
|
||||||
|
self.target.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `Some` if the environment is externally managed, optionally including an error
|
/// Returns `Some` if the environment is externally managed, optionally including an error
|
||||||
/// message from the `EXTERNALLY-MANAGED` file.
|
/// message from the `EXTERNALLY-MANAGED` file.
|
||||||
///
|
///
|
||||||
|
|
@ -148,6 +168,11 @@ impl Interpreter {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're installing into a target directory, it's never externally managed.
|
||||||
|
if self.is_target() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
|
let Ok(contents) = fs::read_to_string(self.stdlib.join("EXTERNALLY-MANAGED")) else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
|
|
@ -303,28 +328,37 @@ impl Interpreter {
|
||||||
self.gil_disabled
|
self.gil_disabled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the `--target` directory for this interpreter, if any.
|
||||||
|
pub fn target(&self) -> Option<&Target> {
|
||||||
|
self.target.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
/// Return the [`Layout`] environment used to install wheels into this interpreter.
|
/// Return the [`Layout`] environment used to install wheels into this interpreter.
|
||||||
pub fn layout(&self) -> Layout {
|
pub fn layout(&self) -> Layout {
|
||||||
Layout {
|
Layout {
|
||||||
python_version: self.python_tuple(),
|
python_version: self.python_tuple(),
|
||||||
sys_executable: self.sys_executable().to_path_buf(),
|
sys_executable: self.sys_executable().to_path_buf(),
|
||||||
os_name: self.markers.os_name.clone(),
|
os_name: self.markers.os_name.clone(),
|
||||||
scheme: Scheme {
|
scheme: if let Some(target) = self.target.as_ref() {
|
||||||
purelib: self.purelib().to_path_buf(),
|
target.scheme()
|
||||||
platlib: self.platlib().to_path_buf(),
|
} else {
|
||||||
scripts: self.scripts().to_path_buf(),
|
Scheme {
|
||||||
data: self.data().to_path_buf(),
|
purelib: self.purelib().to_path_buf(),
|
||||||
include: if self.is_virtualenv() {
|
platlib: self.platlib().to_path_buf(),
|
||||||
// If the interpreter is a venv, then the `include` directory has a different structure.
|
scripts: self.scripts().to_path_buf(),
|
||||||
// See: https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/locations/_sysconfig.py#L172
|
data: self.data().to_path_buf(),
|
||||||
self.prefix.join("include").join("site").join(format!(
|
include: if self.is_virtualenv() {
|
||||||
"python{}.{}",
|
// If the interpreter is a venv, then the `include` directory has a different structure.
|
||||||
self.python_major(),
|
// See: https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/locations/_sysconfig.py#L172
|
||||||
self.python_minor()
|
self.prefix.join("include").join("site").join(format!(
|
||||||
))
|
"python{}.{}",
|
||||||
} else {
|
self.python_major(),
|
||||||
self.include().to_path_buf()
|
self.python_minor()
|
||||||
},
|
))
|
||||||
|
} else {
|
||||||
|
self.include().to_path_buf()
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,12 +19,14 @@ pub use crate::find_python::{find_best_python, find_default_python, find_request
|
||||||
pub use crate::interpreter::Interpreter;
|
pub use crate::interpreter::Interpreter;
|
||||||
use crate::interpreter::InterpreterInfoError;
|
use crate::interpreter::InterpreterInfoError;
|
||||||
pub use crate::python_environment::PythonEnvironment;
|
pub use crate::python_environment::PythonEnvironment;
|
||||||
|
pub use crate::target::Target;
|
||||||
pub use crate::virtualenv::Virtualenv;
|
pub use crate::virtualenv::Virtualenv;
|
||||||
|
|
||||||
mod cfg;
|
mod cfg;
|
||||||
mod find_python;
|
mod find_python;
|
||||||
mod interpreter;
|
mod interpreter;
|
||||||
mod python_environment;
|
mod python_environment;
|
||||||
|
mod target;
|
||||||
mod virtualenv;
|
mod virtualenv;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use itertools::Either;
|
||||||
use std::env;
|
use std::env;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
|
@ -8,7 +9,7 @@ use uv_cache::Cache;
|
||||||
use uv_fs::{LockedFile, Simplified};
|
use uv_fs::{LockedFile, Simplified};
|
||||||
|
|
||||||
use crate::cfg::PyVenvConfiguration;
|
use crate::cfg::PyVenvConfiguration;
|
||||||
use crate::{find_default_python, find_requested_python, Error, Interpreter};
|
use crate::{find_default_python, find_requested_python, Error, Interpreter, Target};
|
||||||
|
|
||||||
/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
|
/// A Python environment, consisting of a Python [`Interpreter`] and its associated paths.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -68,7 +69,16 @@ impl PythonEnvironment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the location of the Python interpreter.
|
/// Create a [`PythonEnvironment`] from an existing [`Interpreter`] and `--target` directory.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_target(self, target: Target) -> Self {
|
||||||
|
Self {
|
||||||
|
interpreter: self.interpreter.with_target(target),
|
||||||
|
..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the root (i.e., `prefix`) of the Python interpreter.
|
||||||
pub fn root(&self) -> &Path {
|
pub fn root(&self) -> &Path {
|
||||||
&self.root
|
&self.root
|
||||||
}
|
}
|
||||||
|
|
@ -97,15 +107,19 @@ impl PythonEnvironment {
|
||||||
/// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
|
/// Some distributions also create symbolic links from `purelib` to `platlib`; in such cases, we
|
||||||
/// still deduplicate the entries, returning a single path.
|
/// still deduplicate the entries, returning a single path.
|
||||||
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
|
pub fn site_packages(&self) -> impl Iterator<Item = &Path> {
|
||||||
let purelib = self.interpreter.purelib();
|
if let Some(target) = self.interpreter.target() {
|
||||||
let platlib = self.interpreter.platlib();
|
Either::Left(std::iter::once(target.root()))
|
||||||
std::iter::once(purelib).chain(
|
} else {
|
||||||
if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
|
let purelib = self.interpreter.purelib();
|
||||||
None
|
let platlib = self.interpreter.platlib();
|
||||||
} else {
|
Either::Right(std::iter::once(purelib).chain(
|
||||||
Some(platlib)
|
if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
|
||||||
},
|
None
|
||||||
)
|
} else {
|
||||||
|
Some(platlib)
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the path to the `bin` directory inside a virtual environment.
|
/// Returns the path to the `bin` directory inside a virtual environment.
|
||||||
|
|
@ -115,7 +129,13 @@ impl PythonEnvironment {
|
||||||
|
|
||||||
/// Grab a file lock for the virtual environment to prevent concurrent writes across processes.
|
/// Grab a file lock for the virtual environment to prevent concurrent writes across processes.
|
||||||
pub fn lock(&self) -> Result<LockedFile, std::io::Error> {
|
pub fn lock(&self) -> Result<LockedFile, std::io::Error> {
|
||||||
if self.interpreter.is_virtualenv() {
|
if let Some(target) = self.interpreter.target() {
|
||||||
|
// If we're installing into a `--target`, use a target-specific lock file.
|
||||||
|
LockedFile::acquire(
|
||||||
|
target.root().join(".lock"),
|
||||||
|
target.root().simplified_display(),
|
||||||
|
)
|
||||||
|
} else if self.interpreter.is_virtualenv() {
|
||||||
// If the environment a virtualenv, use a virtualenv-specific lock file.
|
// If the environment a virtualenv, use a virtualenv-specific lock file.
|
||||||
LockedFile::acquire(self.root.join(".lock"), self.root.simplified_display())
|
LockedFile::acquire(self.root.join(".lock"), self.root.simplified_display())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
40
crates/uv-interpreter/src/target.rs
Normal file
40
crates/uv-interpreter/src/target.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use pypi_types::Scheme;
|
||||||
|
|
||||||
|
/// A `--target` directory into which packages can be installed, separate from a virtual environment
|
||||||
|
/// or system Python interpreter.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Target(PathBuf);
|
||||||
|
|
||||||
|
impl Target {
|
||||||
|
/// Return the [`Scheme`] for the `--target` directory.
|
||||||
|
pub fn scheme(&self) -> Scheme {
|
||||||
|
Scheme {
|
||||||
|
purelib: self.0.clone(),
|
||||||
|
platlib: self.0.clone(),
|
||||||
|
scripts: self.0.join("bin"),
|
||||||
|
data: self.0.clone(),
|
||||||
|
include: self.0.join("include"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Initialize the `--target` directory.
|
||||||
|
pub fn init(&self) -> std::io::Result<()> {
|
||||||
|
fs_err::create_dir_all(&self.0)?;
|
||||||
|
fs_err::create_dir_all(self.0.join("bin"))?;
|
||||||
|
fs_err::create_dir_all(self.0.join("include"))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Return the path to the `--target` directory.
|
||||||
|
pub fn root(&self) -> &Path {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<PathBuf> for Target {
|
||||||
|
fn from(path: PathBuf) -> Self {
|
||||||
|
Self(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,7 @@ pub struct PipOptions {
|
||||||
pub python: Option<String>,
|
pub python: Option<String>,
|
||||||
pub system: Option<bool>,
|
pub system: Option<bool>,
|
||||||
pub break_system_packages: Option<bool>,
|
pub break_system_packages: Option<bool>,
|
||||||
|
pub target: Option<PathBuf>,
|
||||||
pub offline: Option<bool>,
|
pub offline: Option<bool>,
|
||||||
pub index_url: Option<IndexUrl>,
|
pub index_url: Option<IndexUrl>,
|
||||||
pub extra_index_url: Option<Vec<IndexUrl>>,
|
pub extra_index_url: Option<Vec<IndexUrl>>,
|
||||||
|
|
|
||||||
|
|
@ -768,6 +768,11 @@ pub(crate) struct PipSyncArgs {
|
||||||
#[arg(long, overrides_with("break_system_packages"))]
|
#[arg(long, overrides_with("break_system_packages"))]
|
||||||
pub(crate) no_break_system_packages: bool,
|
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)]
|
||||||
|
pub(crate) target: Option<PathBuf>,
|
||||||
|
|
||||||
/// Use legacy `setuptools` behavior when building source distributions without a
|
/// Use legacy `setuptools` behavior when building source distributions without a
|
||||||
/// `pyproject.toml`.
|
/// `pyproject.toml`.
|
||||||
#[arg(long, overrides_with("no_legacy_setup_py"))]
|
#[arg(long, overrides_with("no_legacy_setup_py"))]
|
||||||
|
|
@ -1132,6 +1137,11 @@ pub(crate) struct PipInstallArgs {
|
||||||
#[arg(long, overrides_with("break_system_packages"))]
|
#[arg(long, overrides_with("break_system_packages"))]
|
||||||
pub(crate) no_break_system_packages: bool,
|
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)]
|
||||||
|
pub(crate) target: Option<PathBuf>,
|
||||||
|
|
||||||
/// Use legacy `setuptools` behavior when building source distributions without a
|
/// Use legacy `setuptools` behavior when building source distributions without a
|
||||||
/// `pyproject.toml`.
|
/// `pyproject.toml`.
|
||||||
#[arg(long, overrides_with("no_legacy_setup_py"))]
|
#[arg(long, overrides_with("no_legacy_setup_py"))]
|
||||||
|
|
@ -1335,6 +1345,11 @@ pub(crate) struct PipUninstallArgs {
|
||||||
#[arg(long, overrides_with("break_system_packages"))]
|
#[arg(long, overrides_with("break_system_packages"))]
|
||||||
pub(crate) no_break_system_packages: bool,
|
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)]
|
||||||
|
pub(crate) target: Option<PathBuf>,
|
||||||
|
|
||||||
/// Run offline, i.e., without accessing the network.
|
/// Run offline, i.e., without accessing the network.
|
||||||
#[arg(long, overrides_with("no_offline"))]
|
#[arg(long, overrides_with("no_offline"))]
|
||||||
pub(crate) offline: bool,
|
pub(crate) offline: bool,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anstream::eprint;
|
use anstream::eprint;
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use owo_colors::OwoColorize;
|
use owo_colors::OwoColorize;
|
||||||
use tempfile::tempdir_in;
|
use tempfile::tempdir_in;
|
||||||
|
|
@ -33,7 +31,7 @@ use uv_configuration::{KeyringProviderType, TargetTriple};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::{BuiltEditable, Downloader, Plan, Planner, ResolvedEditable, SitePackages};
|
use uv_installer::{BuiltEditable, Downloader, Plan, Planner, ResolvedEditable, SitePackages};
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment, Target};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_requirements::{
|
use uv_requirements::{
|
||||||
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
|
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
|
||||||
|
|
@ -84,6 +82,7 @@ pub(crate) async fn pip_install(
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
system: bool,
|
system: bool,
|
||||||
break_system_packages: bool,
|
break_system_packages: bool,
|
||||||
|
target: Option<Target>,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
dry_run: bool,
|
dry_run: bool,
|
||||||
|
|
@ -134,6 +133,14 @@ pub(crate) async fn pip_install(
|
||||||
venv.python_executable().user_display().cyan()
|
venv.python_executable().user_display().cyan()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Apply any `--target` directory.
|
||||||
|
let venv = if let Some(target) = target {
|
||||||
|
target.init()?;
|
||||||
|
venv.with_target(target)
|
||||||
|
} else {
|
||||||
|
venv
|
||||||
|
};
|
||||||
|
|
||||||
// If the environment is externally managed, abort.
|
// If the environment is externally managed, abort.
|
||||||
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
||||||
if break_system_packages {
|
if break_system_packages {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ use distribution_types::{
|
||||||
IndexLocations, InstalledMetadata, LocalDist, LocalEditable, LocalEditables, Name, ResolvedDist,
|
IndexLocations, InstalledMetadata, LocalDist, LocalEditable, LocalEditables, Name, ResolvedDist,
|
||||||
};
|
};
|
||||||
use install_wheel_rs::linker::LinkMode;
|
use install_wheel_rs::linker::LinkMode;
|
||||||
|
|
||||||
use platform_tags::Tags;
|
use platform_tags::Tags;
|
||||||
use pypi_types::Yanked;
|
use pypi_types::Yanked;
|
||||||
use requirements_txt::EditableRequirement;
|
use requirements_txt::EditableRequirement;
|
||||||
|
|
@ -27,7 +26,7 @@ use uv_configuration::{KeyringProviderType, TargetTriple};
|
||||||
use uv_dispatch::BuildDispatch;
|
use uv_dispatch::BuildDispatch;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_installer::{is_dynamic, Downloader, Plan, Planner, ResolvedEditable, SitePackages};
|
use uv_installer::{is_dynamic, Downloader, Plan, Planner, ResolvedEditable, SitePackages};
|
||||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
use uv_interpreter::{Interpreter, PythonEnvironment, Target};
|
||||||
use uv_requirements::{
|
use uv_requirements::{
|
||||||
ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
|
ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
|
||||||
SourceTreeResolver,
|
SourceTreeResolver,
|
||||||
|
|
@ -64,6 +63,7 @@ pub(crate) async fn pip_sync(
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
system: bool,
|
system: bool,
|
||||||
break_system_packages: bool,
|
break_system_packages: bool,
|
||||||
|
target: Option<Target>,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
printer: Printer,
|
printer: Printer,
|
||||||
|
|
@ -113,6 +113,14 @@ pub(crate) async fn pip_sync(
|
||||||
venv.python_executable().user_display().cyan()
|
venv.python_executable().user_display().cyan()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Apply any `--target` directory.
|
||||||
|
let venv = if let Some(target) = target {
|
||||||
|
target.init()?;
|
||||||
|
venv.with_target(target)
|
||||||
|
} else {
|
||||||
|
venv
|
||||||
|
};
|
||||||
|
|
||||||
// If the environment is externally managed, abort.
|
// If the environment is externally managed, abort.
|
||||||
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
||||||
if break_system_packages {
|
if break_system_packages {
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,11 @@ use uv_cache::Cache;
|
||||||
use uv_client::{BaseClientBuilder, Connectivity};
|
use uv_client::{BaseClientBuilder, Connectivity};
|
||||||
use uv_configuration::KeyringProviderType;
|
use uv_configuration::KeyringProviderType;
|
||||||
use uv_fs::Simplified;
|
use uv_fs::Simplified;
|
||||||
use uv_interpreter::PythonEnvironment;
|
use uv_interpreter::{PythonEnvironment, Target};
|
||||||
|
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||||
|
|
||||||
use crate::commands::{elapsed, ExitStatus};
|
use crate::commands::{elapsed, ExitStatus};
|
||||||
use crate::printer::Printer;
|
use crate::printer::Printer;
|
||||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
|
||||||
|
|
||||||
/// Uninstall packages from the current environment.
|
/// Uninstall packages from the current environment.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
|
@ -25,6 +25,7 @@ pub(crate) async fn pip_uninstall(
|
||||||
python: Option<String>,
|
python: Option<String>,
|
||||||
system: bool,
|
system: bool,
|
||||||
break_system_packages: bool,
|
break_system_packages: bool,
|
||||||
|
target: Option<Target>,
|
||||||
cache: Cache,
|
cache: Cache,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
|
|
@ -54,6 +55,14 @@ pub(crate) async fn pip_uninstall(
|
||||||
venv.python_executable().user_display().cyan(),
|
venv.python_executable().user_display().cyan(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Apply any `--target` directory.
|
||||||
|
let venv = if let Some(target) = target {
|
||||||
|
target.init()?;
|
||||||
|
venv.with_target(target)
|
||||||
|
} else {
|
||||||
|
venv
|
||||||
|
};
|
||||||
|
|
||||||
// If the environment is externally managed, abort.
|
// If the environment is externally managed, abort.
|
||||||
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
if let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
||||||
if break_system_packages {
|
if break_system_packages {
|
||||||
|
|
|
||||||
|
|
@ -270,6 +270,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.shared.python,
|
args.shared.python,
|
||||||
args.shared.system,
|
args.shared.system,
|
||||||
args.shared.break_system_packages,
|
args.shared.break_system_packages,
|
||||||
|
args.shared.target,
|
||||||
globals.native_tls,
|
globals.native_tls,
|
||||||
cache,
|
cache,
|
||||||
printer,
|
printer,
|
||||||
|
|
@ -334,6 +335,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.shared.python,
|
args.shared.python,
|
||||||
args.shared.system,
|
args.shared.system,
|
||||||
args.shared.break_system_packages,
|
args.shared.break_system_packages,
|
||||||
|
args.shared.target,
|
||||||
globals.native_tls,
|
globals.native_tls,
|
||||||
cache,
|
cache,
|
||||||
args.dry_run,
|
args.dry_run,
|
||||||
|
|
@ -362,6 +364,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.shared.python,
|
args.shared.python,
|
||||||
args.shared.system,
|
args.shared.system,
|
||||||
args.shared.break_system_packages,
|
args.shared.break_system_packages,
|
||||||
|
args.shared.target,
|
||||||
cache,
|
cache,
|
||||||
args.shared.connectivity,
|
args.shared.connectivity,
|
||||||
globals.native_tls,
|
globals.native_tls,
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use uv_configuration::{
|
||||||
ConfigSettings, IndexStrategy, KeyringProviderType, NoBinary, NoBuild, PreviewMode, Reinstall,
|
ConfigSettings, IndexStrategy, KeyringProviderType, NoBinary, NoBuild, PreviewMode, Reinstall,
|
||||||
SetupPyStrategy, TargetTriple, Upgrade,
|
SetupPyStrategy, TargetTriple, Upgrade,
|
||||||
};
|
};
|
||||||
|
use uv_interpreter::Target;
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
use uv_requirements::ExtrasSpecification;
|
use uv_requirements::ExtrasSpecification;
|
||||||
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode};
|
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode};
|
||||||
|
|
@ -302,6 +303,7 @@ impl PipSyncSettings {
|
||||||
no_system,
|
no_system,
|
||||||
break_system_packages,
|
break_system_packages,
|
||||||
no_break_system_packages,
|
no_break_system_packages,
|
||||||
|
target,
|
||||||
legacy_setup_py,
|
legacy_setup_py,
|
||||||
no_legacy_setup_py,
|
no_legacy_setup_py,
|
||||||
no_build_isolation,
|
no_build_isolation,
|
||||||
|
|
@ -332,6 +334,7 @@ impl PipSyncSettings {
|
||||||
python,
|
python,
|
||||||
system: flag(system, no_system),
|
system: flag(system, no_system),
|
||||||
break_system_packages: flag(break_system_packages, no_break_system_packages),
|
break_system_packages: flag(break_system_packages, no_break_system_packages),
|
||||||
|
target,
|
||||||
offline: flag(offline, no_offline),
|
offline: flag(offline, no_offline),
|
||||||
index_url: index_url.and_then(Maybe::into_option),
|
index_url: index_url.and_then(Maybe::into_option),
|
||||||
extra_index_url: extra_index_url.map(|extra_index_urls| {
|
extra_index_url: extra_index_url.map(|extra_index_urls| {
|
||||||
|
|
@ -423,6 +426,7 @@ impl PipInstallSettings {
|
||||||
no_system,
|
no_system,
|
||||||
break_system_packages,
|
break_system_packages,
|
||||||
no_break_system_packages,
|
no_break_system_packages,
|
||||||
|
target,
|
||||||
legacy_setup_py,
|
legacy_setup_py,
|
||||||
no_legacy_setup_py,
|
no_legacy_setup_py,
|
||||||
no_build_isolation,
|
no_build_isolation,
|
||||||
|
|
@ -463,6 +467,7 @@ impl PipInstallSettings {
|
||||||
python,
|
python,
|
||||||
system: flag(system, no_system),
|
system: flag(system, no_system),
|
||||||
break_system_packages: flag(break_system_packages, no_break_system_packages),
|
break_system_packages: flag(break_system_packages, no_break_system_packages),
|
||||||
|
target,
|
||||||
offline: flag(offline, no_offline),
|
offline: flag(offline, no_offline),
|
||||||
index_url: index_url.and_then(Maybe::into_option),
|
index_url: index_url.and_then(Maybe::into_option),
|
||||||
extra_index_url: extra_index_url.map(|extra_index_urls| {
|
extra_index_url: extra_index_url.map(|extra_index_urls| {
|
||||||
|
|
@ -530,6 +535,7 @@ impl PipUninstallSettings {
|
||||||
no_system,
|
no_system,
|
||||||
break_system_packages,
|
break_system_packages,
|
||||||
no_break_system_packages,
|
no_break_system_packages,
|
||||||
|
target,
|
||||||
offline,
|
offline,
|
||||||
no_offline,
|
no_offline,
|
||||||
} = args;
|
} = args;
|
||||||
|
|
@ -545,6 +551,7 @@ impl PipUninstallSettings {
|
||||||
python,
|
python,
|
||||||
system: flag(system, no_system),
|
system: flag(system, no_system),
|
||||||
break_system_packages: flag(break_system_packages, no_break_system_packages),
|
break_system_packages: flag(break_system_packages, no_break_system_packages),
|
||||||
|
target,
|
||||||
offline: flag(offline, no_offline),
|
offline: flag(offline, no_offline),
|
||||||
keyring_provider,
|
keyring_provider,
|
||||||
..PipOptions::default()
|
..PipOptions::default()
|
||||||
|
|
@ -801,6 +808,7 @@ pub(crate) struct PipSharedSettings {
|
||||||
pub(crate) system: bool,
|
pub(crate) system: bool,
|
||||||
pub(crate) extras: ExtrasSpecification,
|
pub(crate) extras: ExtrasSpecification,
|
||||||
pub(crate) break_system_packages: bool,
|
pub(crate) break_system_packages: bool,
|
||||||
|
pub(crate) target: Option<Target>,
|
||||||
pub(crate) connectivity: Connectivity,
|
pub(crate) connectivity: Connectivity,
|
||||||
pub(crate) index_strategy: IndexStrategy,
|
pub(crate) index_strategy: IndexStrategy,
|
||||||
pub(crate) keyring_provider: KeyringProviderType,
|
pub(crate) keyring_provider: KeyringProviderType,
|
||||||
|
|
@ -840,6 +848,7 @@ impl PipSharedSettings {
|
||||||
python,
|
python,
|
||||||
system,
|
system,
|
||||||
break_system_packages,
|
break_system_packages,
|
||||||
|
target,
|
||||||
offline,
|
offline,
|
||||||
index_url,
|
index_url,
|
||||||
extra_index_url,
|
extra_index_url,
|
||||||
|
|
@ -955,6 +964,7 @@ impl PipSharedSettings {
|
||||||
.break_system_packages
|
.break_system_packages
|
||||||
.or(break_system_packages)
|
.or(break_system_packages)
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
target: args.target.or(target).map(Target::from),
|
||||||
no_binary: NoBinary::from_args(args.no_binary.or(no_binary).unwrap_or_default()),
|
no_binary: NoBinary::from_args(args.no_binary.or(no_binary).unwrap_or_default()),
|
||||||
compile_bytecode: args
|
compile_bytecode: args
|
||||||
.compile_bytecode
|
.compile_bytecode
|
||||||
|
|
|
||||||
|
|
@ -4808,3 +4808,95 @@ fn require_hashes_registry_invalid_hash() -> Result<()> {
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sync to a `--target` directory.
|
||||||
|
#[test]
|
||||||
|
fn target() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
// Install `iniconfig` to the target directory.
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("iniconfig==2.0.0")?;
|
||||||
|
|
||||||
|
uv_snapshot!(command(&context)
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--target")
|
||||||
|
.arg("target"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Downloaded 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ iniconfig==2.0.0
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Ensure that the package is present in the target directory.
|
||||||
|
assert!(context.temp_dir.child("target").child("iniconfig").is_dir());
|
||||||
|
|
||||||
|
// Ensure that we can't import the package.
|
||||||
|
context.assert_command("import iniconfig").failure();
|
||||||
|
|
||||||
|
// Ensure that we can import the package by augmenting the `PYTHONPATH`.
|
||||||
|
Command::new(venv_to_interpreter(&context.venv))
|
||||||
|
.arg("-B")
|
||||||
|
.arg("-c")
|
||||||
|
.arg("import iniconfig")
|
||||||
|
.env("PYTHONPATH", context.temp_dir.child("target").path())
|
||||||
|
.current_dir(&context.temp_dir)
|
||||||
|
.assert()
|
||||||
|
.success();
|
||||||
|
|
||||||
|
// Upgrade it.
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("iniconfig==1.1.1")?;
|
||||||
|
|
||||||
|
uv_snapshot!(command(&context)
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--target")
|
||||||
|
.arg("target"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Downloaded 1 package in [TIME]
|
||||||
|
Uninstalled 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
- iniconfig==2.0.0
|
||||||
|
+ iniconfig==1.1.1
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Remove it, and replace with `flask`, which includes a binary.
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("flask")?;
|
||||||
|
|
||||||
|
uv_snapshot!(command(&context)
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--target")
|
||||||
|
.arg("target"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
Downloaded 1 package in [TIME]
|
||||||
|
Uninstalled 1 package in [TIME]
|
||||||
|
Installed 1 package in [TIME]
|
||||||
|
+ flask==3.0.3
|
||||||
|
- iniconfig==1.1.1
|
||||||
|
"###);
|
||||||
|
// Ensure that the binary is present in the target directory.
|
||||||
|
assert!(context
|
||||||
|
.temp_dir
|
||||||
|
.child("target")
|
||||||
|
.child("bin")
|
||||||
|
.child(format!("flask{EXE_SUFFIX}"))
|
||||||
|
.is_file());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
||||||
6
uv.schema.json
generated
6
uv.schema.json
generated
|
|
@ -489,6 +489,12 @@
|
||||||
"boolean",
|
"boolean",
|
||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"target": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue