diff --git a/crates/uv-distribution-types/src/installed.rs b/crates/uv-distribution-types/src/installed.rs index 4c291013c..9024d3d21 100644 --- a/crates/uv-distribution-types/src/installed.rs +++ b/crates/uv-distribution-types/src/installed.rs @@ -19,7 +19,8 @@ use uv_pypi_types::{DirectUrl, MetadataError}; use uv_redacted::DisplaySafeUrl; use crate::{ - BuildInfo, DistributionMetadata, InstalledMetadata, InstalledVersion, Name, VersionOrUrlRef, + BuildInfo, DistributionMetadata, InstalledMetadata, InstalledVersion, Name, VariantsJson, + VersionOrUrlRef, }; #[derive(Error, Debug)] @@ -484,6 +485,19 @@ impl InstalledDist { } } + /// Read the `variant.json` file of the distribution, if it exists. + pub fn read_variant_json(&self) -> Result, InstalledDistError> { + let path = self.install_path().join("variant.json"); + let file = match fs_err::File::open(&path) { + Ok(file) => file, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(err) => return Err(err.into()), + }; + let variants_json = + serde_json::from_reader::, VariantsJson>(BufReader::new(file))?; + Ok(Some(variants_json)) + } + /// Return the supported wheel tags for the distribution from the `WHEEL` file, if available. pub fn read_tags(&self) -> Result, InstalledDistError> { if let Some(tags) = self.tags_cache.get() { diff --git a/crates/uv-distribution-types/src/variant_json.rs b/crates/uv-distribution-types/src/variant_json.rs index c19cf78a6..ab5b10bfb 100644 --- a/crates/uv-distribution-types/src/variant_json.rs +++ b/crates/uv-distribution-types/src/variant_json.rs @@ -1,18 +1,38 @@ +use rustc_hash::FxHashMap; use std::{fmt::Display, str::FromStr}; use uv_normalize::{InvalidNameError, PackageName}; use uv_pep440::{Version, VersionParseError}; #[derive(Debug, thiserror::Error)] pub enum VariantsJsonError { - #[error("Invalid variants.json filename")] + #[error("Invalid `variants.json` filename")] InvalidFilename, - #[error("Invalid variants.json package name: {0}")] + #[error("Invalid `variants.json` package name: {0}")] InvalidName(#[from] InvalidNameError), - #[error("Invalid variants.json version: {0}")] + #[error("Invalid `variants.json` version: {0}")] InvalidVersion(#[from] VersionParseError), } /// A `--variants.json` file. +#[derive(Debug, Clone, serde::Deserialize)] +pub struct VariantsJson { + variants: FxHashMap, +} + +impl VariantsJson { + /// Returns the label for the current variant. + pub fn label(&self) -> Option<&str> { + let mut keys = self.variants.keys(); + let label = keys.next()?; + if keys.next().is_some() { + None + } else { + Some(label) + } + } +} + +/// A `--variants.json` filename. #[derive( Debug, Clone, diff --git a/crates/uv/src/commands/pip/list.rs b/crates/uv/src/commands/pip/list.rs index 5287c1672..fe1ebfb36 100644 --- a/crates/uv/src/commands/pip/list.rs +++ b/crates/uv/src/commands/pip/list.rs @@ -19,6 +19,7 @@ use uv_configuration::{Concurrency, IndexStrategy, KeyringProviderType}; use uv_distribution_filename::DistFilename; use uv_distribution_types::{ Diagnostic, IndexCapabilities, IndexLocations, InstalledDist, Name, RequiresPython, + VariantsJson, }; use uv_fs::Simplified; use uv_installer::SitePackages; @@ -166,6 +167,13 @@ pub(crate) async fn pip_list( .map(|dist| Entry { name: dist.name().clone(), version: dist.version().clone(), + variant: dist + .read_variant_json() + .ok() + .flatten() + .as_ref() + .and_then(VariantsJson::label) + .map(ToString::to_string), latest_version: latest .get(dist.name()) .and_then(|filename| filename.as_ref()) @@ -204,6 +212,28 @@ pub(crate) async fn pip_list( }, ]; + // Variant column is only displayed if at least one package has a variant. + let variants = results + .iter() + .map(|dist| { + dist.read_variant_json() + .ok() + .flatten() + .as_ref() + .and_then(VariantsJson::label) + .map(ToString::to_string) + }) + .collect_vec(); + if variants.iter().any(Option::is_some) { + columns.push(Column { + header: String::from("Variant"), + rows: variants + .into_iter() + .map(std::option::Option::unwrap_or_default) + .collect_vec(), + }); + } + // The latest version and type are only displayed if outdated. if outdated { columns.push(Column { @@ -330,6 +360,8 @@ struct Entry { name: PackageName, version: Version, #[serde(skip_serializing_if = "Option::is_none")] + variant: Option, + #[serde(skip_serializing_if = "Option::is_none")] latest_version: Option, #[serde(skip_serializing_if = "Option::is_none")] latest_filetype: Option,