mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +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 }
|
||||
fs-err = { workspace = true, features = ["tokio"] }
|
||||
itertools = { workspace = true }
|
||||
once_cell = { workspace = true }
|
||||
regex = { 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_toolchain::PythonVersion;
|
||||
|
||||
use crate::Error;
|
||||
use crate::Virtualenv;
|
||||
use crate::{Error, Target};
|
||||
|
||||
/// A Python executable and its associated platform markers.
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -35,6 +35,7 @@ pub struct Interpreter {
|
|||
sys_executable: PathBuf,
|
||||
stdlib: PathBuf,
|
||||
tags: OnceCell<Tags>,
|
||||
target: Option<Target>,
|
||||
gil_disabled: bool,
|
||||
}
|
||||
|
||||
|
@ -62,6 +63,7 @@ impl Interpreter {
|
|||
sys_executable: info.sys_executable,
|
||||
stdlib: info.stdlib,
|
||||
tags: OnceCell::new(),
|
||||
target: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -91,6 +93,7 @@ impl Interpreter {
|
|||
sys_executable: PathBuf::from("/dev/null"),
|
||||
stdlib: PathBuf::from("/dev/null"),
|
||||
tags: OnceCell::new(),
|
||||
target: None,
|
||||
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.
|
||||
#[inline]
|
||||
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>
|
||||
pub fn is_virtualenv(&self) -> bool {
|
||||
// Maybe this should return `false` if it's a target?
|
||||
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
|
||||
/// message from the `EXTERNALLY-MANAGED` file.
|
||||
///
|
||||
|
@ -148,6 +168,11 @@ impl Interpreter {
|
|||
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 {
|
||||
return None;
|
||||
};
|
||||
|
@ -303,28 +328,37 @@ impl Interpreter {
|
|||
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.
|
||||
pub fn layout(&self) -> Layout {
|
||||
Layout {
|
||||
python_version: self.python_tuple(),
|
||||
sys_executable: self.sys_executable().to_path_buf(),
|
||||
os_name: self.markers.os_name.clone(),
|
||||
scheme: Scheme {
|
||||
purelib: self.purelib().to_path_buf(),
|
||||
platlib: self.platlib().to_path_buf(),
|
||||
scripts: self.scripts().to_path_buf(),
|
||||
data: self.data().to_path_buf(),
|
||||
include: if self.is_virtualenv() {
|
||||
// If the interpreter is a venv, then the `include` directory has a different structure.
|
||||
// See: https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/locations/_sysconfig.py#L172
|
||||
self.prefix.join("include").join("site").join(format!(
|
||||
"python{}.{}",
|
||||
self.python_major(),
|
||||
self.python_minor()
|
||||
))
|
||||
} else {
|
||||
self.include().to_path_buf()
|
||||
},
|
||||
scheme: if let Some(target) = self.target.as_ref() {
|
||||
target.scheme()
|
||||
} else {
|
||||
Scheme {
|
||||
purelib: self.purelib().to_path_buf(),
|
||||
platlib: self.platlib().to_path_buf(),
|
||||
scripts: self.scripts().to_path_buf(),
|
||||
data: self.data().to_path_buf(),
|
||||
include: if self.is_virtualenv() {
|
||||
// If the interpreter is a venv, then the `include` directory has a different structure.
|
||||
// See: https://github.com/pypa/pip/blob/0ad4c94be74cc24874c6feb5bb3c2152c398a18e/src/pip/_internal/locations/_sysconfig.py#L172
|
||||
self.prefix.join("include").join("site").join(format!(
|
||||
"python{}.{}",
|
||||
self.python_major(),
|
||||
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;
|
||||
use crate::interpreter::InterpreterInfoError;
|
||||
pub use crate::python_environment::PythonEnvironment;
|
||||
pub use crate::target::Target;
|
||||
pub use crate::virtualenv::Virtualenv;
|
||||
|
||||
mod cfg;
|
||||
mod find_python;
|
||||
mod interpreter;
|
||||
mod python_environment;
|
||||
mod target;
|
||||
mod virtualenv;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
use itertools::Either;
|
||||
use std::env;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
|
@ -8,7 +9,7 @@ use uv_cache::Cache;
|
|||
use uv_fs::{LockedFile, Simplified};
|
||||
|
||||
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.
|
||||
#[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 {
|
||||
&self.root
|
||||
}
|
||||
|
@ -97,15 +107,19 @@ 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> {
|
||||
let purelib = self.interpreter.purelib();
|
||||
let platlib = self.interpreter.platlib();
|
||||
std::iter::once(purelib).chain(
|
||||
if purelib == platlib || is_same_file(purelib, platlib).unwrap_or(false) {
|
||||
None
|
||||
} else {
|
||||
Some(platlib)
|
||||
},
|
||||
)
|
||||
if let Some(target) = self.interpreter.target() {
|
||||
Either::Left(std::iter::once(target.root()))
|
||||
} else {
|
||||
let purelib = self.interpreter.purelib();
|
||||
let platlib = self.interpreter.platlib();
|
||||
Either::Right(std::iter::once(purelib).chain(
|
||||
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.
|
||||
|
@ -115,7 +129,13 @@ impl PythonEnvironment {
|
|||
|
||||
/// Grab a file lock for the virtual environment to prevent concurrent writes across processes.
|
||||
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.
|
||||
LockedFile::acquire(self.root.join(".lock"), self.root.simplified_display())
|
||||
} 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 system: Option<bool>,
|
||||
pub break_system_packages: Option<bool>,
|
||||
pub target: Option<PathBuf>,
|
||||
pub offline: Option<bool>,
|
||||
pub index_url: Option<IndexUrl>,
|
||||
pub extra_index_url: Option<Vec<IndexUrl>>,
|
||||
|
|
|
@ -768,6 +768,11 @@ pub(crate) struct PipSyncArgs {
|
|||
#[arg(long, overrides_with("break_system_packages"))]
|
||||
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
|
||||
/// `pyproject.toml`.
|
||||
#[arg(long, overrides_with("no_legacy_setup_py"))]
|
||||
|
@ -1132,6 +1137,11 @@ pub(crate) struct PipInstallArgs {
|
|||
#[arg(long, overrides_with("break_system_packages"))]
|
||||
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
|
||||
/// `pyproject.toml`.
|
||||
#[arg(long, overrides_with("no_legacy_setup_py"))]
|
||||
|
@ -1335,6 +1345,11 @@ 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)]
|
||||
pub(crate) target: Option<PathBuf>,
|
||||
|
||||
/// Run offline, i.e., without accessing the network.
|
||||
#[arg(long, overrides_with("no_offline"))]
|
||||
pub(crate) offline: bool,
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
use std::borrow::Cow;
|
||||
use std::fmt::Write;
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anstream::eprint;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
|
||||
use itertools::Itertools;
|
||||
use owo_colors::OwoColorize;
|
||||
use tempfile::tempdir_in;
|
||||
|
@ -33,7 +31,7 @@ use uv_configuration::{KeyringProviderType, TargetTriple};
|
|||
use uv_dispatch::BuildDispatch;
|
||||
use uv_fs::Simplified;
|
||||
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_requirements::{
|
||||
ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, RequirementsSource,
|
||||
|
@ -84,6 +82,7 @@ pub(crate) async fn pip_install(
|
|||
python: Option<String>,
|
||||
system: bool,
|
||||
break_system_packages: bool,
|
||||
target: Option<Target>,
|
||||
native_tls: bool,
|
||||
cache: Cache,
|
||||
dry_run: bool,
|
||||
|
@ -134,6 +133,14 @@ pub(crate) async fn pip_install(
|
|||
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 let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
||||
if break_system_packages {
|
||||
|
|
|
@ -11,7 +11,6 @@ use distribution_types::{
|
|||
IndexLocations, InstalledMetadata, LocalDist, LocalEditable, LocalEditables, Name, ResolvedDist,
|
||||
};
|
||||
use install_wheel_rs::linker::LinkMode;
|
||||
|
||||
use platform_tags::Tags;
|
||||
use pypi_types::Yanked;
|
||||
use requirements_txt::EditableRequirement;
|
||||
|
@ -27,7 +26,7 @@ use uv_configuration::{KeyringProviderType, TargetTriple};
|
|||
use uv_dispatch::BuildDispatch;
|
||||
use uv_fs::Simplified;
|
||||
use uv_installer::{is_dynamic, Downloader, Plan, Planner, ResolvedEditable, SitePackages};
|
||||
use uv_interpreter::{Interpreter, PythonEnvironment};
|
||||
use uv_interpreter::{Interpreter, PythonEnvironment, Target};
|
||||
use uv_requirements::{
|
||||
ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification,
|
||||
SourceTreeResolver,
|
||||
|
@ -64,6 +63,7 @@ pub(crate) async fn pip_sync(
|
|||
python: Option<String>,
|
||||
system: bool,
|
||||
break_system_packages: bool,
|
||||
target: Option<Target>,
|
||||
native_tls: bool,
|
||||
cache: Cache,
|
||||
printer: Printer,
|
||||
|
@ -113,6 +113,14 @@ pub(crate) async fn pip_sync(
|
|||
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 let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
||||
if break_system_packages {
|
||||
|
|
|
@ -12,11 +12,11 @@ use uv_cache::Cache;
|
|||
use uv_client::{BaseClientBuilder, Connectivity};
|
||||
use uv_configuration::KeyringProviderType;
|
||||
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::printer::Printer;
|
||||
use uv_requirements::{RequirementsSource, RequirementsSpecification};
|
||||
|
||||
/// Uninstall packages from the current environment.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
@ -25,6 +25,7 @@ pub(crate) async fn pip_uninstall(
|
|||
python: Option<String>,
|
||||
system: bool,
|
||||
break_system_packages: bool,
|
||||
target: Option<Target>,
|
||||
cache: Cache,
|
||||
connectivity: Connectivity,
|
||||
native_tls: bool,
|
||||
|
@ -54,6 +55,14 @@ pub(crate) async fn pip_uninstall(
|
|||
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 let Some(externally_managed) = venv.interpreter().is_externally_managed() {
|
||||
if break_system_packages {
|
||||
|
|
|
@ -270,6 +270,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
args.shared.python,
|
||||
args.shared.system,
|
||||
args.shared.break_system_packages,
|
||||
args.shared.target,
|
||||
globals.native_tls,
|
||||
cache,
|
||||
printer,
|
||||
|
@ -334,6 +335,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
args.shared.python,
|
||||
args.shared.system,
|
||||
args.shared.break_system_packages,
|
||||
args.shared.target,
|
||||
globals.native_tls,
|
||||
cache,
|
||||
args.dry_run,
|
||||
|
@ -362,6 +364,7 @@ async fn run() -> Result<ExitStatus> {
|
|||
args.shared.python,
|
||||
args.shared.system,
|
||||
args.shared.break_system_packages,
|
||||
args.shared.target,
|
||||
cache,
|
||||
args.shared.connectivity,
|
||||
globals.native_tls,
|
||||
|
|
|
@ -9,6 +9,7 @@ use uv_configuration::{
|
|||
ConfigSettings, IndexStrategy, KeyringProviderType, NoBinary, NoBuild, PreviewMode, Reinstall,
|
||||
SetupPyStrategy, TargetTriple, Upgrade,
|
||||
};
|
||||
use uv_interpreter::Target;
|
||||
use uv_normalize::PackageName;
|
||||
use uv_requirements::ExtrasSpecification;
|
||||
use uv_resolver::{AnnotationStyle, DependencyMode, ExcludeNewer, PreReleaseMode, ResolutionMode};
|
||||
|
@ -302,6 +303,7 @@ impl PipSyncSettings {
|
|||
no_system,
|
||||
break_system_packages,
|
||||
no_break_system_packages,
|
||||
target,
|
||||
legacy_setup_py,
|
||||
no_legacy_setup_py,
|
||||
no_build_isolation,
|
||||
|
@ -332,6 +334,7 @@ impl PipSyncSettings {
|
|||
python,
|
||||
system: flag(system, no_system),
|
||||
break_system_packages: flag(break_system_packages, no_break_system_packages),
|
||||
target,
|
||||
offline: flag(offline, no_offline),
|
||||
index_url: index_url.and_then(Maybe::into_option),
|
||||
extra_index_url: extra_index_url.map(|extra_index_urls| {
|
||||
|
@ -423,6 +426,7 @@ impl PipInstallSettings {
|
|||
no_system,
|
||||
break_system_packages,
|
||||
no_break_system_packages,
|
||||
target,
|
||||
legacy_setup_py,
|
||||
no_legacy_setup_py,
|
||||
no_build_isolation,
|
||||
|
@ -463,6 +467,7 @@ impl PipInstallSettings {
|
|||
python,
|
||||
system: flag(system, no_system),
|
||||
break_system_packages: flag(break_system_packages, no_break_system_packages),
|
||||
target,
|
||||
offline: flag(offline, no_offline),
|
||||
index_url: index_url.and_then(Maybe::into_option),
|
||||
extra_index_url: extra_index_url.map(|extra_index_urls| {
|
||||
|
@ -530,6 +535,7 @@ impl PipUninstallSettings {
|
|||
no_system,
|
||||
break_system_packages,
|
||||
no_break_system_packages,
|
||||
target,
|
||||
offline,
|
||||
no_offline,
|
||||
} = args;
|
||||
|
@ -545,6 +551,7 @@ impl PipUninstallSettings {
|
|||
python,
|
||||
system: flag(system, no_system),
|
||||
break_system_packages: flag(break_system_packages, no_break_system_packages),
|
||||
target,
|
||||
offline: flag(offline, no_offline),
|
||||
keyring_provider,
|
||||
..PipOptions::default()
|
||||
|
@ -801,6 +808,7 @@ pub(crate) struct PipSharedSettings {
|
|||
pub(crate) system: bool,
|
||||
pub(crate) extras: ExtrasSpecification,
|
||||
pub(crate) break_system_packages: bool,
|
||||
pub(crate) target: Option<Target>,
|
||||
pub(crate) connectivity: Connectivity,
|
||||
pub(crate) index_strategy: IndexStrategy,
|
||||
pub(crate) keyring_provider: KeyringProviderType,
|
||||
|
@ -840,6 +848,7 @@ impl PipSharedSettings {
|
|||
python,
|
||||
system,
|
||||
break_system_packages,
|
||||
target,
|
||||
offline,
|
||||
index_url,
|
||||
extra_index_url,
|
||||
|
@ -955,6 +964,7 @@ impl PipSharedSettings {
|
|||
.break_system_packages
|
||||
.or(break_system_packages)
|
||||
.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()),
|
||||
compile_bytecode: args
|
||||
.compile_bytecode
|
||||
|
|
|
@ -4808,3 +4808,95 @@ fn require_hashes_registry_invalid_hash() -> Result<()> {
|
|||
|
||||
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",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"target": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue