use crate::source::SourceLocation;
use crate::UrlOrPath;
use rattler_conda_types::package::DistArchiveIdentifier;
use rattler_conda_types::{
ChannelUrl, MatchSpec, Matches, NamelessMatchSpec, PackageRecord, RepoDataRecord,
};
use rattler_digest::Sha256Hash;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::{borrow::Cow, cmp::Ordering, fmt::Display, hash::Hash};
use typed_path::Utf8TypedPathBuf;
use url::Url;
#[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(CondaBinaryData),
Source(CondaSourceData),
}
impl CondaPackageData {
pub fn location(&self) -> &UrlOrPath {
match self {
Self::Binary(data) => &data.location,
Self::Source(data) => &data.location,
}
}
pub fn record(&self) -> &PackageRecord {
match self {
CondaPackageData::Binary(data) => &data.package_record,
CondaPackageData::Source(data) => &data.package_record,
}
}
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),
}
}
pub(crate) fn merge(&self, other: &Self) -> Cow<'_, Self> {
match (self, other) {
(CondaPackageData::Binary(left), CondaPackageData::Binary(right)) => {
if let Cow::Owned(merged) = left.merge(right) {
return Cow::Owned(merged.into());
}
}
(CondaPackageData::Source(left), CondaPackageData::Source(right)) => {
if let Cow::Owned(merged) = left.merge(right) {
return Cow::Owned(merged.into());
}
}
_ => {}
}
Cow::Borrowed(self)
}
}
#[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(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)]
pub enum GitShallowSpec {
Branch(String),
Tag(String),
Rev,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
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 CondaSourceData {
pub package_record: PackageRecord,
pub location: UrlOrPath,
pub variants: Option<BTreeMap<String, VariantValue>>,
pub package_build_source: Option<PackageBuildSource>,
pub input: Option<InputHash>,
pub sources: BTreeMap<String, SourceLocation>,
}
impl From<CondaSourceData> for CondaPackageData {
fn from(value: CondaSourceData) -> Self {
Self::Source(value)
}
}
impl CondaSourceData {
pub(crate) fn merge(&self, other: &Self) -> Cow<'_, Self> {
if self.location == other.location {
let package_record_merge =
merge_package_record(&self.package_record, &other.package_record);
let package_build_source_merge =
merge_package_build_source(&self.package_build_source, &other.package_build_source);
if matches!(package_record_merge, Cow::Owned(_))
|| matches!(package_build_source_merge, Cow::Owned(_))
{
return Cow::Owned(Self {
package_record: package_record_merge.into_owned(),
package_build_source: package_build_source_merge.into_owned(),
..self.clone()
});
}
}
Cow::Borrowed(self)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct InputHash {
pub hash: Sha256Hash,
pub globs: Vec<String>,
}
impl AsRef<PackageRecord> for CondaPackageData {
fn as_ref(&self) -> &PackageRecord {
match self {
Self::Binary(data) => &data.package_record,
Self::Source(data) => &data.package_record,
}
}
}
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 {
let pkg_a: &PackageRecord = self.as_ref();
let pkg_b: &PackageRecord = other.as_ref();
let location_a = self.location();
let location_b = other.location();
location_a
.cmp(location_b)
.then_with(|| pkg_a.name.cmp(&pkg_b.name))
.then_with(|| 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))
}
}
impl From<RepoDataRecord> for CondaPackageData {
fn from(value: RepoDataRecord) -> Self {
let location = UrlOrPath::from(value.url).normalize().into_owned();
Self::Binary(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),
}
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.record().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;
}
}
}
spec.matches(self.record())
}
}
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;
}
}
}
spec.matches(self.record())
}
}
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
}
fn merge_package_build_source<'a>(
left: &'a Option<PackageBuildSource>,
right: &Option<PackageBuildSource>,
) -> Cow<'a, Option<PackageBuildSource>> {
if left == right {
Cow::Borrowed(left)
} else if let Some(right_source) = right {
Cow::Owned(Some(right_source.clone()))
} else {
Cow::Borrowed(left)
}
}