use std::path::Path;
use std::str::FromStr;
use fs_err::File;
use tracing::{instrument, trace};
use uv_distribution_filename::WheelFilename;
use uv_pep440::Version;
use uv_pypi_types::{DirectUrl, Metadata10};
use crate::linker::{InstallState, LinkMode, link_wheel_files};
use crate::wheel::{
LibKind, WheelFile, dist_info_metadata, find_dist_info, install_data, parse_scripts,
read_record_file, write_installer_metadata, write_script_entrypoints,
};
use crate::{Error, Layout};
#[instrument(skip_all, fields(wheel = %filename))]
pub fn install_wheel<Cache: serde::Serialize, Build: serde::Serialize>(
layout: &Layout,
relocatable: bool,
wheel: impl AsRef<Path>,
filename: &WheelFilename,
direct_url: Option<&DirectUrl>,
cache_info: Option<&Cache>,
build_info: Option<&Build>,
installer: Option<&str>,
installer_metadata: bool,
link_mode: LinkMode,
state: &InstallState,
) -> Result<(), Error> {
let dist_info_prefix = find_dist_info(&wheel)?;
let metadata = dist_info_metadata(&dist_info_prefix, &wheel)?;
let Metadata10 { name, version } = Metadata10::parse_pkg_info(&metadata)
.map_err(|err| Error::InvalidWheel(err.to_string()))?;
let version = Version::from_str(&version)?;
if !uv_flags::contains(uv_flags::EnvironmentFlags::SKIP_WHEEL_FILENAME_CHECK) {
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()));
}
}
let wheel_file_path = wheel
.as_ref()
.join(format!("{dist_info_prefix}.dist-info/WHEEL"));
let wheel_text = fs_err::read_to_string(wheel_file_path)?;
let lib_kind = WheelFile::parse(&wheel_text)?.lib_kind();
trace!(?name, "Extracting file");
let site_packages = match lib_kind {
LibKind::Pure => &layout.scheme.purelib,
LibKind::Plat => &layout.scheme.platlib,
};
let num_unpacked = link_wheel_files(link_mode, site_packages, &wheel, state, filename)?;
trace!(?name, "Extracted {num_unpacked} files");
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,
)?;
}
let data_dir = site_packages.join(format!("{dist_info_prefix}.data"));
if data_dir.is_dir() {
install_data(
layout,
relocatable,
site_packages,
&data_dir,
&name,
&console_scripts,
&gui_scripts,
&mut record,
)?;
fs_err::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,
build_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(())
}