use rattler_conda_types::package::DistArchiveIdentifier;
use std::{borrow::Cow, cmp::Ordering, collections::BTreeMap, fmt::Display, hash::Hash};
use rattler_conda_types::{
ChannelUrl, MatchSpec, Matches, NamelessMatchSpec, PackageRecord, RepoDataRecord,
};
use rattler_digest::Sha256Hash;
use serde::{Deserialize, Serialize};
use typed_path::Utf8TypedPathBuf;
use url::Url;
use crate::{source::SourceLocation, SourceTimestamps, UrlOrPath};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum VariantValue {
String(String),
Int(i64),
Bool(bool),
}
impl PartialOrd for VariantValue {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for VariantValue {
fn cmp(&self, other: &Self) -> Ordering {
#[allow(clippy::match_same_arms)]
match (self, other) {
(VariantValue::String(a), VariantValue::String(b)) => a.cmp(b),
(VariantValue::Int(a), VariantValue::Int(b)) => a.cmp(b),
(VariantValue::Bool(a), VariantValue::Bool(b)) => a.cmp(b),
(VariantValue::String(_), _) => Ordering::Less,
(_, VariantValue::String(_)) => Ordering::Greater,
(VariantValue::Int(_), VariantValue::Bool(_)) => Ordering::Less,
(VariantValue::Bool(_), VariantValue::Int(_)) => Ordering::Greater,
}
}
}
impl Display for VariantValue {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VariantValue::String(s) => write!(f, "{s}"),
VariantValue::Int(i) => write!(f, "{i}"),
VariantValue::Bool(b) => write!(f, "{b}"),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum CondaPackageData {
Binary(Box<CondaBinaryData>),
Source(Box<CondaSourceData>),
}
impl CondaPackageData {
pub fn location(&self) -> &UrlOrPath {
match self {
Self::Binary(data) => &data.location,
Self::Source(data) => &data.location,
}
}
pub fn record(&self) -> Option<&PackageRecord> {
match self {
CondaPackageData::Binary(data) => Some(&data.package_record),
CondaPackageData::Source(data) => data.record(),
}
}
pub fn name(&self) -> &rattler_conda_types::PackageName {
match self {
CondaPackageData::Binary(data) => &data.package_record.name,
CondaPackageData::Source(data) => data.name(),
}
}
pub fn depends(&self) -> &[String] {
match self {
CondaPackageData::Binary(data) => &data.package_record.depends,
CondaPackageData::Source(data) => data.depends(),
}
}
pub fn as_binary(&self) -> Option<&CondaBinaryData> {
match self {
Self::Binary(data) => Some(data),
Self::Source(_) => None,
}
}
pub fn as_source(&self) -> Option<&CondaSourceData> {
match self {
Self::Binary(_) => None,
Self::Source(data) => Some(data),
}
}
pub fn into_binary(self) -> Option<CondaBinaryData> {
match self {
Self::Binary(data) => Some(*data),
Self::Source(_) => None,
}
}
pub fn into_source(self) -> Option<CondaSourceData> {
match self {
Self::Binary(_) => None,
Self::Source(data) => Some(*data),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CondaBinaryData {
pub package_record: PackageRecord,
pub location: UrlOrPath,
pub file_name: DistArchiveIdentifier,
pub channel: Option<ChannelUrl>,
}
impl From<CondaBinaryData> for CondaPackageData {
fn from(value: CondaBinaryData) -> Self {
Self::Binary(Box::new(value))
}
}
impl CondaBinaryData {
pub(crate) fn merge(&self, other: &Self) -> Cow<'_, Self> {
if self.location == other.location {
if let Cow::Owned(merged) =
merge_package_record(&self.package_record, &other.package_record)
{
return Cow::Owned(Self {
package_record: merged,
..self.clone()
});
}
}
Cow::Borrowed(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum GitShallowSpec {
Branch(String),
Tag(String),
Rev,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum PackageBuildSource {
Git {
url: Url,
spec: Option<GitShallowSpec>,
rev: String,
subdir: Option<Utf8TypedPathBuf>,
},
Url {
url: Url,
sha256: Sha256Hash,
subdir: Option<Utf8TypedPathBuf>,
},
Path {
path: Utf8TypedPathBuf,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PartialSourceMetadata {
pub name: rattler_conda_types::PackageName,
pub depends: Vec<String>,
pub sources: BTreeMap<String, SourceLocation>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct FullSourceMetadata {
pub package_record: PackageRecord,
pub sources: BTreeMap<String, SourceLocation>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum SourceMetadata {
Partial(PartialSourceMetadata),
Full(Box<FullSourceMetadata>),
}
impl SourceMetadata {
pub fn as_full(&self) -> Option<&FullSourceMetadata> {
match self {
Self::Full(full) => Some(full),
Self::Partial(_) => None,
}
}
pub fn as_partial(&self) -> Option<&PartialSourceMetadata> {
match self {
Self::Partial(partial) => Some(partial),
Self::Full(_) => None,
}
}
}
#[derive(Clone, Debug)]
pub struct CondaSourceData<D = SourceMetadata> {
pub location: UrlOrPath,
pub package_build_source: Option<PackageBuildSource>,
pub variants: BTreeMap<String, VariantValue>,
pub timestamp: Option<SourceTimestamps>,
pub identifier_hash: Option<String>,
pub metadata: D,
}
impl<D: PartialEq> PartialEq for CondaSourceData<D> {
fn eq(&self, other: &Self) -> bool {
self.location == other.location
&& self.package_build_source == other.package_build_source
&& self.variants == other.variants
&& self.metadata == other.metadata
}
}
impl<D: Eq> Eq for CondaSourceData<D> {}
impl<D: Hash> std::hash::Hash for CondaSourceData<D> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.location.hash(state);
self.package_build_source.hash(state);
self.variants.hash(state);
self.metadata.hash(state);
}
}
impl CondaSourceData<SourceMetadata> {
pub fn name(&self) -> &rattler_conda_types::PackageName {
match &self.metadata {
SourceMetadata::Partial(p) => &p.name,
SourceMetadata::Full(f) => &f.package_record.name,
}
}
pub fn record(&self) -> Option<&PackageRecord> {
self.metadata.as_full().map(|f| &f.package_record)
}
pub fn sources(&self) -> &BTreeMap<String, SourceLocation> {
match &self.metadata {
SourceMetadata::Full(f) => &f.sources,
SourceMetadata::Partial(p) => &p.sources,
}
}
pub fn depends(&self) -> &[String] {
match &self.metadata {
SourceMetadata::Full(f) => &f.package_record.depends,
SourceMetadata::Partial(p) => &p.depends,
}
}
pub fn into_full(self) -> Option<CondaSourceData<FullSourceMetadata>> {
match self.metadata {
SourceMetadata::Full(full) => Some(CondaSourceData {
location: self.location,
package_build_source: self.package_build_source,
variants: self.variants,
timestamp: self.timestamp,
identifier_hash: self.identifier_hash,
metadata: *full,
}),
SourceMetadata::Partial(_) => None,
}
}
pub fn full(
location: UrlOrPath,
package_build_source: Option<PackageBuildSource>,
variants: BTreeMap<String, VariantValue>,
timestamp: Option<SourceTimestamps>,
identifier_hash: Option<String>,
package_record: PackageRecord,
sources: BTreeMap<String, SourceLocation>,
) -> Self {
Self {
location,
package_build_source,
variants,
timestamp,
identifier_hash,
metadata: SourceMetadata::Full(Box::new(FullSourceMetadata {
package_record,
sources,
})),
}
}
#[allow(clippy::too_many_arguments)]
pub fn partial(
location: UrlOrPath,
package_build_source: Option<PackageBuildSource>,
variants: BTreeMap<String, VariantValue>,
timestamp: Option<SourceTimestamps>,
identifier_hash: Option<String>,
name: rattler_conda_types::PackageName,
depends: Vec<String>,
sources: BTreeMap<String, SourceLocation>,
) -> Self {
Self {
location,
package_build_source,
variants,
timestamp,
identifier_hash,
metadata: SourceMetadata::Partial(PartialSourceMetadata {
name,
depends,
sources,
}),
}
}
}
impl CondaSourceData<FullSourceMetadata> {
pub fn name(&self) -> &rattler_conda_types::PackageName {
&self.metadata.package_record.name
}
pub fn record(&self) -> &PackageRecord {
&self.metadata.package_record
}
pub fn depends(&self) -> &[String] {
&self.metadata.package_record.depends
}
pub fn sources(&self) -> &BTreeMap<String, SourceLocation> {
&self.metadata.sources
}
}
impl CondaSourceData<PartialSourceMetadata> {
pub fn name(&self) -> &rattler_conda_types::PackageName {
&self.metadata.name
}
pub fn depends(&self) -> &[String] {
&self.metadata.depends
}
pub fn sources(&self) -> &BTreeMap<String, SourceLocation> {
&self.metadata.sources
}
}
impl From<CondaSourceData<FullSourceMetadata>> for CondaSourceData<SourceMetadata> {
fn from(value: CondaSourceData<FullSourceMetadata>) -> Self {
Self {
location: value.location,
package_build_source: value.package_build_source,
variants: value.variants,
timestamp: value.timestamp,
identifier_hash: value.identifier_hash,
metadata: SourceMetadata::Full(Box::new(value.metadata)),
}
}
}
impl From<CondaSourceData<PartialSourceMetadata>> for CondaSourceData<SourceMetadata> {
fn from(value: CondaSourceData<PartialSourceMetadata>) -> Self {
Self {
location: value.location,
package_build_source: value.package_build_source,
variants: value.variants,
timestamp: value.timestamp,
identifier_hash: value.identifier_hash,
metadata: SourceMetadata::Partial(value.metadata),
}
}
}
impl From<CondaSourceData> for CondaPackageData {
fn from(value: CondaSourceData) -> Self {
Self::Source(Box::new(value))
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct InputHash {
pub hash: Sha256Hash,
pub globs: Vec<String>,
}
impl PartialOrd<Self> for CondaPackageData {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for CondaPackageData {
fn cmp(&self, other: &Self) -> Ordering {
self.location()
.cmp(other.location())
.then_with(|| self.name().cmp(other.name()))
.then_with(|| match (self.record(), other.record()) {
(Some(pkg_a), Some(pkg_b)) => pkg_a
.version
.cmp(&pkg_b.version)
.then_with(|| pkg_a.build.cmp(&pkg_b.build))
.then_with(|| pkg_a.subdir.cmp(&pkg_b.subdir)),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
})
.then_with(|| match (self.as_source(), other.as_source()) {
(Some(src_a), Some(src_b)) => src_a
.variants
.cmp(&src_b.variants)
.then_with(|| src_a.package_build_source.cmp(&src_b.package_build_source))
.then_with(|| match (&src_a.timestamp, &src_b.timestamp) {
(Some(a), Some(b)) => (a.latest).cmp(&b.latest),
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
(None, None) => Ordering::Equal,
}),
_ => Ordering::Equal,
})
}
}
impl From<RepoDataRecord> for CondaPackageData {
fn from(value: RepoDataRecord) -> Self {
let location = UrlOrPath::from(value.url).normalize().into_owned();
Self::Binary(Box::new(CondaBinaryData {
package_record: value.package_record,
file_name: value.identifier,
channel: value
.channel
.and_then(|channel| Url::parse(&channel).ok())
.map(Into::into),
location,
}))
}
}
impl TryFrom<&CondaBinaryData> for RepoDataRecord {
type Error = ConversionError;
fn try_from(value: &CondaBinaryData) -> Result<Self, Self::Error> {
Self::try_from(value.clone())
}
}
impl TryFrom<CondaBinaryData> for RepoDataRecord {
type Error = ConversionError;
fn try_from(value: CondaBinaryData) -> Result<Self, Self::Error> {
Ok(Self {
package_record: value.package_record,
identifier: value.file_name,
url: value.location.try_into_url()?,
channel: value.channel.map(|channel| channel.to_string()),
})
}
}
#[derive(thiserror::Error, Debug)]
pub enum ConversionError {
#[error("missing field/fields '{0}'")]
Missing(String),
#[error(transparent)]
LocationToUrlConversionError(#[from] file_url::FileURLParseError),
#[error("binary package location must have a valid archive filename (.conda or .tar.bz2)")]
InvalidBinaryPackageLocation,
}
impl CondaPackageData {
pub fn satisfies(&self, spec: &MatchSpec) -> bool {
self.matches(spec)
}
}
impl Matches<MatchSpec> for CondaPackageData {
fn matches(&self, spec: &MatchSpec) -> bool {
if !spec.name.matches(self.name()) {
return false;
}
if let Some(channel) = &spec.channel {
match self {
CondaPackageData::Binary(binary) => {
if let Some(record_channel) = &binary.channel {
if &channel.base_url != record_channel {
return false;
}
}
}
CondaPackageData::Source(_) => {
return false;
}
}
}
match self.record() {
Some(record) => spec.matches(record),
None => {
spec.version.is_none()
&& spec.build.is_none()
&& spec.build_number.is_none()
&& spec.subdir.is_none()
&& spec.md5.is_none()
&& spec.sha256.is_none()
}
}
}
}
impl Matches<NamelessMatchSpec> for CondaPackageData {
fn matches(&self, spec: &NamelessMatchSpec) -> bool {
if let Some(channel) = &spec.channel {
match self {
CondaPackageData::Binary(binary) => {
if let Some(record_channel) = &binary.channel {
if &channel.base_url != record_channel {
return false;
}
}
}
CondaPackageData::Source(_) => {
return false;
}
}
}
match self.record() {
Some(record) => spec.matches(record),
None => {
spec.version.is_none()
&& spec.build.is_none()
&& spec.build_number.is_none()
&& spec.subdir.is_none()
&& spec.md5.is_none()
&& spec.sha256.is_none()
}
}
}
}
fn merge_package_record<'a>(
left: &'a PackageRecord,
right: &PackageRecord,
) -> Cow<'a, PackageRecord> {
let mut result = Cow::Borrowed(left);
if left.purls.is_none() && right.purls.is_some() {
result = Cow::Owned(PackageRecord {
purls: right.purls.clone(),
..result.into_owned()
});
}
if left.run_exports.is_none() && right.run_exports.is_some() {
result = Cow::Owned(PackageRecord {
run_exports: right.run_exports.clone(),
..result.into_owned()
});
}
if left.md5.is_none() && right.md5.is_some() {
result = Cow::Owned(PackageRecord {
md5: right.md5,
..result.into_owned()
});
}
if left.sha256.is_none() && right.sha256.is_some() {
result = Cow::Owned(PackageRecord {
sha256: right.sha256,
..result.into_owned()
});
}
result
}