uv-install-wheel: Split installation logic and link logic (#11166)

uv-install-wheel had the logic for laying out the installation and for
linking a directory in the same module. We split them up to isolate each
module's logic and tighten the crate's interface to only expose top
level members.

No logic changes, only moving code around.
This commit is contained in:
konsti 2025-02-02 16:02:13 +01:00 committed by GitHub
parent cca1d34432
commit 1cfe5be355
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 256 additions and 246 deletions

View File

@ -92,7 +92,7 @@ mod resolver {
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::DistributionDatabase;
use uv_distribution_types::{DependencyMetadata, IndexLocations};
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_pep440::Version;
use uv_pep508::{MarkerEnvironment, MarkerEnvironmentBuilder};
use uv_platform_tags::{Arch, Os, Platform, Tags};

View File

@ -2445,7 +2445,7 @@ pub struct VenvArgs {
/// Defaults to `clone` (also known as Copy-on-Write) on macOS, and `hardlink` on Linux and
/// Windows.
#[arg(long, value_enum, env = EnvVars::UV_LINK_MODE)]
pub link_mode: Option<uv_install_wheel::linker::LinkMode>,
pub link_mode: Option<uv_install_wheel::LinkMode>,
#[command(flatten)]
pub refresh: RefreshArgs,
@ -4170,7 +4170,7 @@ pub struct ToolUpgradeArgs {
env = EnvVars::UV_LINK_MODE,
help_heading = "Installer options"
)]
pub link_mode: Option<uv_install_wheel::linker::LinkMode>,
pub link_mode: Option<uv_install_wheel::LinkMode>,
/// Compile Python files to bytecode after installation.
///
@ -4790,7 +4790,7 @@ pub struct InstallerArgs {
env = EnvVars::UV_LINK_MODE,
help_heading = "Installer options"
)]
pub link_mode: Option<uv_install_wheel::linker::LinkMode>,
pub link_mode: Option<uv_install_wheel::LinkMode>,
/// Compile Python files to bytecode after installation.
///
@ -4986,7 +4986,7 @@ pub struct ResolverArgs {
env = EnvVars::UV_LINK_MODE,
help_heading = "Installer options"
)]
pub link_mode: Option<uv_install_wheel::linker::LinkMode>,
pub link_mode: Option<uv_install_wheel::LinkMode>,
/// Ignore the `tool.uv.sources` table when resolving dependencies. Used to lock against the
/// standards-compliant, publishable package metadata, as opposed to using any workspace, Git,
@ -5174,7 +5174,7 @@ pub struct ResolverInstallerArgs {
env = EnvVars::UV_LINK_MODE,
help_heading = "Installer options"
)]
pub link_mode: Option<uv_install_wheel::linker::LinkMode>,
pub link_mode: Option<uv_install_wheel::LinkMode>,
/// Compile Python files to bytecode after installation.
///

View File

@ -86,7 +86,7 @@ pub struct BuildDispatch<'a> {
shared_state: SharedState,
dependency_metadata: &'a DependencyMetadata,
build_isolation: BuildIsolation<'a>,
link_mode: uv_install_wheel::linker::LinkMode,
link_mode: uv_install_wheel::LinkMode,
build_options: &'a BuildOptions,
config_settings: &'a ConfigSettings,
hasher: &'a HashStrategy,
@ -112,7 +112,7 @@ impl<'a> BuildDispatch<'a> {
index_strategy: IndexStrategy,
config_settings: &'a ConfigSettings,
build_isolation: BuildIsolation<'a>,
link_mode: uv_install_wheel::linker::LinkMode,
link_mode: uv_install_wheel::LinkMode,
build_options: &'a BuildOptions,
hasher: &'a HashStrategy,
exclude_newer: Option<ExcludeNewer>,

View File

@ -0,0 +1,224 @@
//! Like `wheel.rs`, but for installing wheels that have already been unzipped, rather than
//! reading from a zip file.
use std::path::Path;
use crate::linker::{LinkMode, Locks};
use crate::script::{scripts_from_ini, Script};
use crate::wheel::{
install_data, parse_wheel_file, read_record_file, write_installer_metadata,
write_script_entrypoints, LibKind,
};
use crate::{Error, Layout};
use fs_err as fs;
use fs_err::File;
use tracing::{instrument, trace};
use uv_cache_info::CacheInfo;
use uv_distribution_filename::WheelFilename;
use uv_pypi_types::{DirectUrl, Metadata12};
/// Install the given wheel to the given venv
///
/// The caller must ensure that the wheel is compatible to the environment.
///
/// <https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl>
///
/// Wheel 1.0: <https://www.python.org/dev/peps/pep-0427/>
#[instrument(skip_all, fields(wheel = %filename))]
pub fn install_wheel(
layout: &Layout,
relocatable: bool,
wheel: impl AsRef<Path>,
filename: &WheelFilename,
direct_url: Option<&DirectUrl>,
cache_info: Option<&CacheInfo>,
installer: Option<&str>,
installer_metadata: bool,
link_mode: LinkMode,
locks: &Locks,
) -> Result<(), Error> {
let dist_info_prefix = find_dist_info(&wheel)?;
let metadata = dist_info_metadata(&dist_info_prefix, &wheel)?;
let Metadata12 { name, version, .. } = Metadata12::parse_metadata(&metadata)
.map_err(|err| Error::InvalidWheel(err.to_string()))?;
// Validate the wheel name and version.
{
if name != filename.name {
return Err(Error::MismatchedName(name, filename.name.clone()));
}
if version != filename.version && version != filename.version.clone().without_local() {
return Err(Error::MismatchedVersion(version, filename.version.clone()));
}
}
// We're going step by step though
// https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl
// > 1.a Parse distribution-1.0.dist-info/WHEEL.
// > 1.b Check that installer is compatible with Wheel-Version. Warn if minor version is greater, abort if major version is greater.
let wheel_file_path = wheel
.as_ref()
.join(format!("{dist_info_prefix}.dist-info/WHEEL"));
let wheel_text = fs::read_to_string(wheel_file_path)?;
let lib_kind = parse_wheel_file(&wheel_text)?;
// > 1.c If Root-Is-Purelib == true, unpack archive into purelib (site-packages).
// > 1.d Else unpack archive into platlib (site-packages).
trace!(?name, "Extracting file");
let site_packages = match lib_kind {
LibKind::Pure => &layout.scheme.purelib,
LibKind::Plat => &layout.scheme.platlib,
};
let num_unpacked = link_mode.link_wheel_files(site_packages, &wheel, locks)?;
trace!(?name, "Extracted {num_unpacked} files");
// Read the RECORD file.
let mut record_file = File::open(
wheel
.as_ref()
.join(format!("{dist_info_prefix}.dist-info/RECORD")),
)?;
let mut record = read_record_file(&mut record_file)?;
let (console_scripts, gui_scripts) =
parse_scripts(&wheel, &dist_info_prefix, None, layout.python_version.1)?;
if console_scripts.is_empty() && gui_scripts.is_empty() {
trace!(?name, "No entrypoints");
} else {
trace!(?name, "Writing entrypoints");
fs_err::create_dir_all(&layout.scheme.scripts)?;
write_script_entrypoints(
layout,
relocatable,
site_packages,
&console_scripts,
&mut record,
false,
)?;
write_script_entrypoints(
layout,
relocatable,
site_packages,
&gui_scripts,
&mut record,
true,
)?;
}
// 2.a Unpacked archive includes distribution-1.0.dist-info/ and (if there is data) distribution-1.0.data/.
// 2.b Move each subtree of distribution-1.0.data/ onto its destination path. Each subdirectory of distribution-1.0.data/ is a key into a dict of destination directories, such as distribution-1.0.data/(purelib|platlib|headers|scripts|data). The initially supported paths are taken from distutils.command.install.
let data_dir = site_packages.join(format!("{dist_info_prefix}.data"));
if data_dir.is_dir() {
trace!(?name, "Installing data");
install_data(
layout,
relocatable,
site_packages,
&data_dir,
&name,
&console_scripts,
&gui_scripts,
&mut record,
)?;
// 2.c If applicable, update scripts starting with #!python to point to the correct interpreter.
// Script are unsupported through data
// 2.e Remove empty distribution-1.0.data directory.
fs::remove_dir_all(data_dir)?;
} else {
trace!(?name, "No data");
}
if installer_metadata {
trace!(?name, "Writing installer metadata");
write_installer_metadata(
site_packages,
&dist_info_prefix,
true,
direct_url,
cache_info,
installer,
&mut record,
)?;
}
trace!(?name, "Writing record");
let mut record_writer = csv::WriterBuilder::new()
.has_headers(false)
.escape(b'"')
.from_path(site_packages.join(format!("{dist_info_prefix}.dist-info/RECORD")))?;
record.sort();
for entry in record {
record_writer.serialize(entry)?;
}
Ok(())
}
/// Find the `dist-info` directory in an unzipped wheel.
///
/// See: <https://github.com/PyO3/python-pkginfo-rs>
///
/// See: <https://github.com/pypa/pip/blob/36823099a9cdd83261fdbc8c1d2a24fa2eea72ca/src/pip/_internal/utils/wheel.py#L38>
fn find_dist_info(path: impl AsRef<Path>) -> Result<String, Error> {
// Iterate over `path` to find the `.dist-info` directory. It should be at the top-level.
let Some(dist_info) = fs::read_dir(path.as_ref())?.find_map(|entry| {
let entry = entry.ok()?;
let file_type = entry.file_type().ok()?;
if file_type.is_dir() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "dist-info") {
Some(path)
} else {
None
}
} else {
None
}
}) else {
return Err(Error::InvalidWheel(
"Missing .dist-info directory".to_string(),
));
};
let Some(dist_info_prefix) = dist_info.file_stem() else {
return Err(Error::InvalidWheel(
"Missing .dist-info directory".to_string(),
));
};
Ok(dist_info_prefix.to_string_lossy().to_string())
}
/// Read the `dist-info` metadata from a directory.
fn dist_info_metadata(dist_info_prefix: &str, wheel: impl AsRef<Path>) -> Result<Vec<u8>, Error> {
let metadata_file = wheel
.as_ref()
.join(format!("{dist_info_prefix}.dist-info/METADATA"));
Ok(fs::read(metadata_file)?)
}
/// Parses the `entry_points.txt` entry in the wheel for console scripts
///
/// Returns (`script_name`, module, function)
///
/// Extras are supposed to be ignored, which happens if you pass None for extras.
fn parse_scripts(
wheel: impl AsRef<Path>,
dist_info_prefix: &str,
extras: Option<&[String]>,
python_minor: u8,
) -> Result<(Vec<Script>, Vec<Script>), Error> {
let entry_points_path = wheel
.as_ref()
.join(format!("{dist_info_prefix}.dist-info/entry_points.txt"));
// Read the entry points mapping. If the file doesn't exist, we just return an empty mapping.
let Ok(ini) = fs::read_to_string(entry_points_path) else {
return Ok((Vec::new(), Vec::new()));
};
scripts_from_ini(extras, python_minor, ini)
}

View File

@ -7,15 +7,19 @@ use platform_info::PlatformInfoError;
use thiserror::Error;
use zip::result::ZipError;
pub use uninstall::{uninstall_egg, uninstall_legacy_editable, uninstall_wheel, Uninstall};
use uv_fs::Simplified;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_platform_tags::{Arch, Os};
use uv_pypi_types::Scheme;
pub use install::install_wheel;
pub use linker::{LinkMode, Locks};
pub use uninstall::{uninstall_egg, uninstall_legacy_editable, uninstall_wheel, Uninstall};
pub use wheel::{parse_wheel_file, read_record_file, LibKind};
pub mod linker;
mod install;
mod linker;
mod record;
mod script;
mod uninstall;

View File

@ -1,238 +1,20 @@
//! Like `wheel.rs`, but for installing wheels that have already been unzipped, rather than
//! reading from a zip file.
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use crate::script::{scripts_from_ini, Script};
use crate::wheel::{
install_data, parse_wheel_file, read_record_file, write_installer_metadata,
write_script_entrypoints, LibKind,
};
use crate::{Error, Layout};
use crate::Error;
use fs_err as fs;
use fs_err::{DirEntry, File};
use fs_err::DirEntry;
use reflink_copy as reflink;
use rustc_hash::FxHashMap;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use tempfile::tempdir_in;
use tracing::{debug, instrument, trace};
use uv_cache_info::CacheInfo;
use uv_distribution_filename::WheelFilename;
use uv_pypi_types::{DirectUrl, Metadata12};
use uv_warnings::warn_user_once;
use walkdir::WalkDir;
#[derive(Debug, Default)]
pub struct Locks(Mutex<FxHashMap<PathBuf, Arc<Mutex<()>>>>);
/// Install the given wheel to the given venv
///
/// The caller must ensure that the wheel is compatible to the environment.
///
/// <https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl>
///
/// Wheel 1.0: <https://www.python.org/dev/peps/pep-0427/>
#[instrument(skip_all, fields(wheel = %filename))]
pub fn install_wheel(
layout: &Layout,
relocatable: bool,
wheel: impl AsRef<Path>,
filename: &WheelFilename,
direct_url: Option<&DirectUrl>,
cache_info: Option<&CacheInfo>,
installer: Option<&str>,
installer_metadata: bool,
link_mode: LinkMode,
locks: &Locks,
) -> Result<(), Error> {
let dist_info_prefix = find_dist_info(&wheel)?;
let metadata = dist_info_metadata(&dist_info_prefix, &wheel)?;
let Metadata12 { name, version, .. } = Metadata12::parse_metadata(&metadata)
.map_err(|err| Error::InvalidWheel(err.to_string()))?;
// Validate the wheel name and version.
{
if name != filename.name {
return Err(Error::MismatchedName(name, filename.name.clone()));
}
if version != filename.version && version != filename.version.clone().without_local() {
return Err(Error::MismatchedVersion(version, filename.version.clone()));
}
}
// We're going step by step though
// https://packaging.python.org/en/latest/specifications/binary-distribution-format/#installing-a-wheel-distribution-1-0-py32-none-any-whl
// > 1.a Parse distribution-1.0.dist-info/WHEEL.
// > 1.b Check that installer is compatible with Wheel-Version. Warn if minor version is greater, abort if major version is greater.
let wheel_file_path = wheel
.as_ref()
.join(format!("{dist_info_prefix}.dist-info/WHEEL"));
let wheel_text = fs::read_to_string(wheel_file_path)?;
let lib_kind = parse_wheel_file(&wheel_text)?;
// > 1.c If Root-Is-Purelib == true, unpack archive into purelib (site-packages).
// > 1.d Else unpack archive into platlib (site-packages).
trace!(?name, "Extracting file");
let site_packages = match lib_kind {
LibKind::Pure => &layout.scheme.purelib,
LibKind::Plat => &layout.scheme.platlib,
};
let num_unpacked = link_mode.link_wheel_files(site_packages, &wheel, locks)?;
trace!(?name, "Extracted {num_unpacked} files");
// Read the RECORD file.
let mut record_file = File::open(
wheel
.as_ref()
.join(format!("{dist_info_prefix}.dist-info/RECORD")),
)?;
let mut record = read_record_file(&mut record_file)?;
let (console_scripts, gui_scripts) =
parse_scripts(&wheel, &dist_info_prefix, None, layout.python_version.1)?;
if console_scripts.is_empty() && gui_scripts.is_empty() {
trace!(?name, "No entrypoints");
} else {
trace!(?name, "Writing entrypoints");
fs_err::create_dir_all(&layout.scheme.scripts)?;
write_script_entrypoints(
layout,
relocatable,
site_packages,
&console_scripts,
&mut record,
false,
)?;
write_script_entrypoints(
layout,
relocatable,
site_packages,
&gui_scripts,
&mut record,
true,
)?;
}
// 2.a Unpacked archive includes distribution-1.0.dist-info/ and (if there is data) distribution-1.0.data/.
// 2.b Move each subtree of distribution-1.0.data/ onto its destination path. Each subdirectory of distribution-1.0.data/ is a key into a dict of destination directories, such as distribution-1.0.data/(purelib|platlib|headers|scripts|data). The initially supported paths are taken from distutils.command.install.
let data_dir = site_packages.join(format!("{dist_info_prefix}.data"));
if data_dir.is_dir() {
trace!(?name, "Installing data");
install_data(
layout,
relocatable,
site_packages,
&data_dir,
&name,
&console_scripts,
&gui_scripts,
&mut record,
)?;
// 2.c If applicable, update scripts starting with #!python to point to the correct interpreter.
// Script are unsupported through data
// 2.e Remove empty distribution-1.0.data directory.
fs::remove_dir_all(data_dir)?;
} else {
trace!(?name, "No data");
}
if installer_metadata {
trace!(?name, "Writing installer metadata");
write_installer_metadata(
site_packages,
&dist_info_prefix,
true,
direct_url,
cache_info,
installer,
&mut record,
)?;
}
trace!(?name, "Writing record");
let mut record_writer = csv::WriterBuilder::new()
.has_headers(false)
.escape(b'"')
.from_path(site_packages.join(format!("{dist_info_prefix}.dist-info/RECORD")))?;
record.sort();
for entry in record {
record_writer.serialize(entry)?;
}
Ok(())
}
/// Find the `dist-info` directory in an unzipped wheel.
///
/// See: <https://github.com/PyO3/python-pkginfo-rs>
///
/// See: <https://github.com/pypa/pip/blob/36823099a9cdd83261fdbc8c1d2a24fa2eea72ca/src/pip/_internal/utils/wheel.py#L38>
fn find_dist_info(path: impl AsRef<Path>) -> Result<String, Error> {
// Iterate over `path` to find the `.dist-info` directory. It should be at the top-level.
let Some(dist_info) = fs::read_dir(path.as_ref())?.find_map(|entry| {
let entry = entry.ok()?;
let file_type = entry.file_type().ok()?;
if file_type.is_dir() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "dist-info") {
Some(path)
} else {
None
}
} else {
None
}
}) else {
return Err(Error::InvalidWheel(
"Missing .dist-info directory".to_string(),
));
};
let Some(dist_info_prefix) = dist_info.file_stem() else {
return Err(Error::InvalidWheel(
"Missing .dist-info directory".to_string(),
));
};
Ok(dist_info_prefix.to_string_lossy().to_string())
}
/// Read the `dist-info` metadata from a directory.
fn dist_info_metadata(dist_info_prefix: &str, wheel: impl AsRef<Path>) -> Result<Vec<u8>, Error> {
let metadata_file = wheel
.as_ref()
.join(format!("{dist_info_prefix}.dist-info/METADATA"));
Ok(fs::read(metadata_file)?)
}
/// Parses the `entry_points.txt` entry in the wheel for console scripts
///
/// Returns (`script_name`, module, function)
///
/// Extras are supposed to be ignored, which happens if you pass None for extras.
fn parse_scripts(
wheel: impl AsRef<Path>,
dist_info_prefix: &str,
extras: Option<&[String]>,
python_minor: u8,
) -> Result<(Vec<Script>, Vec<Script>), Error> {
let entry_points_path = wheel
.as_ref()
.join(format!("{dist_info_prefix}.dist-info/entry_points.txt"));
// Read the entry points mapping. If the file doesn't exist, we just return an empty mapping.
let Ok(ini) = fs::read_to_string(entry_points_path) else {
return Ok((Vec::new(), Vec::new()));
};
scripts_from_ini(extras, python_minor, ini)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "kebab-case")]
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]

View File

@ -9,7 +9,7 @@ use tracing::instrument;
use uv_cache::Cache;
use uv_configuration::RAYON_INITIALIZE;
use uv_distribution_types::CachedDist;
use uv_install_wheel::{linker::LinkMode, Layout};
use uv_install_wheel::{Layout, LinkMode};
use uv_python::PythonEnvironment;
pub struct Installer<'a> {
@ -34,7 +34,7 @@ impl<'a> Installer<'a> {
}
}
/// Set the [`LinkMode`][`uv_install_wheel::linker::LinkMode`] to use for this installer.
/// Set the [`LinkMode`][`uv_install_wheel::LinkMode`] to use for this installer.
#[must_use]
pub fn with_link_mode(self, link_mode: LinkMode) -> Self {
Self { link_mode, ..self }
@ -158,9 +158,9 @@ fn install(
) -> Result<Vec<CachedDist>> {
// Initialize the threadpool with the user settings.
LazyLock::force(&RAYON_INITIALIZE);
let locks = uv_install_wheel::linker::Locks::default();
let locks = uv_install_wheel::Locks::default();
wheels.par_iter().try_for_each(|wheel| {
uv_install_wheel::linker::install_wheel(
uv_install_wheel::install_wheel(
&layout,
relocatable,
wheel.path(),

View File

@ -8,7 +8,7 @@ use uv_configuration::{
TrustedPublishing,
};
use uv_distribution_types::{Index, IndexUrl, PipExtraIndex, PipFindLinks, PipIndex};
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_pypi_types::{SchemaConflicts, SupportedEnvironments};
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};

View File

@ -11,7 +11,7 @@ use uv_configuration::{
use uv_distribution_types::{
Index, IndexUrl, IndexUrlError, PipExtraIndex, PipFindLinks, PipIndex, StaticMetadata,
};
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_macros::{CombineOptions, OptionsMetadata};
use uv_normalize::{ExtraName, PackageName};
use uv_pep508::Requirement;

View File

@ -31,7 +31,7 @@ use uv_distribution_filename::{
};
use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, SourceDist};
use uv_fs::{relative_to, Simplified};
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_normalize::PackageName;
use uv_pep440::Version;
use uv_python::{

View File

@ -23,7 +23,7 @@ use uv_distribution_types::{
Origin, UnresolvedRequirementSpecification, Verbatim,
};
use uv_fs::Simplified;
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_normalize::PackageName;
use uv_pypi_types::{Conflicts, Requirement, SupportedEnvironments};
use uv_python::{

View File

@ -20,7 +20,7 @@ use uv_distribution_types::{
UnresolvedRequirementSpecification,
};
use uv_fs::Simplified;
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_installer::{SatisfiesResult, SitePackages};
use uv_pep508::PackageName;
use uv_pypi_types::{Conflicts, Requirement};

View File

@ -26,7 +26,7 @@ use uv_distribution_types::{
DistributionMetadata, IndexLocations, InstalledMetadata, Name, Resolution,
};
use uv_fs::Simplified;
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_installer::{Plan, Planner, Preparer, SitePackages};
use uv_normalize::PackageName;
use uv_platform_tags::Tags;

View File

@ -17,7 +17,7 @@ use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, Origin, Resolution};
use uv_fs::Simplified;
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_installer::SitePackages;
use uv_pep508::PackageName;
use uv_pypi_types::Conflicts;

View File

@ -19,7 +19,7 @@ use uv_configuration::{
use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution_types::{DependencyMetadata, Index, IndexLocations};
use uv_fs::Simplified;
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_pypi_types::Requirement;
use uv_python::{
EnvironmentPreference, PythonDownloads, PythonInstallation, PythonPreference, PythonRequest,

View File

@ -28,7 +28,7 @@ use uv_configuration::{
SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade, VersionControlSystem,
};
use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl};
use uv_install_wheel::linker::LinkMode;
use uv_install_wheel::LinkMode;
use uv_normalize::PackageName;
use uv_pep508::{ExtraName, RequirementOrigin};
use uv_pypi_types::{Requirement, SupportedEnvironments};