Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions crates/rattler_conda_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,9 @@ pub use prefix_record::PrefixRecord;
pub use record_traits::HasArtifactIdentificationRefs;
pub use repo_data::{
ChannelInfo, ChannelRelations, ConvertSubdirError, PackageRecord, RecordFromPath, RepoData,
RepodataRevision, RepodataRevisionInfo, SubdirRunExportsJson, UrlOrPath, V3Packages,
ValidatePackageRecordsError, WhlPackageRecord, compute_package_url,
RepodataRevision, RepodataRevisionInfo, RepodataRevisionMetadata, RepodataRevisions,
SubdirRunExportsJson, UrlOrPath, V3Packages, ValidatePackageRecordsError, WhlPackageRecord,
compute_package_url,
patches::{PackageRecordPatch, PatchInstructions, RepoDataPatch},
sharded::{Shard, ShardedRepodata, ShardedSubdirInfo},
};
Expand Down
92 changes: 70 additions & 22 deletions crates/rattler_conda_types/src/repo_data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ use indexmap::IndexMap;
use rattler_digest::{Md5Hash, Sha256Hash, serde::SerializableHash};
use rattler_macros::sorted;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use serde_with::{DeserializeFromStr, SerializeDisplay, serde_as, skip_serializing_none};
use serde_with::{
DeserializeFromStr, DisplayFromStr, SerializeDisplay, serde_as, skip_serializing_none,
};
use thiserror::Error;
use url::Url;

Expand Down Expand Up @@ -82,6 +84,7 @@ pub struct RepoData {
}

/// Information about subdirectory of channel in the Conda [`RepoData`]
#[serde_as]
#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]
pub struct ChannelInfo {
/// The channel's subdirectory
Expand All @@ -93,22 +96,44 @@ pub struct ChannelInfo {

/// Repodata revisions available in this repodata file.
///
/// See <https://github.com/conda/ceps/pull/146>.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub repodata_revisions: Vec<RepodataRevisionInfo>,
/// Serialized as a `vN`-keyed dictionary per the CEP draft
/// <https://github.com/conda/ceps/pull/146>.
#[serde_as(as = "IndexMap<DisplayFromStr, _>")]
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub repodata_revisions: RepodataRevisions,

/// Optional relationships to other channels as defined in
/// [CEP-42](https://github.com/conda/ceps/blob/main/cep-0042.md).
#[serde(default, skip_serializing_if = "ChannelRelations::is_none_or_empty")]
pub channel_relations: Option<ChannelRelations>,
}

/// Metadata for a repodata revision advertised in
/// `info.repodata_revisions`.
///
/// Future repodata revisions are encoded in parallel top-level `vN` maps. This
/// metadata lets older clients tell users that the channel contains newer
/// records that may be invisible to the current client.
/// Repodata revisions keyed by revision, mirroring the `vN` dictionary of the
/// CEP draft <https://github.com/conda/ceps/pull/146>. Keying encodes
/// uniqueness; insertion order is preserved.
pub type RepodataRevisions = IndexMap<RepodataRevision, RepodataRevisionMetadata>;

/// Metadata for a single [`RepodataRevisions`] entry; the revision itself is
/// the map key.
#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone, Default)]
pub struct RepodataRevisionMetadata {
/// The number of packages available in this revision.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub n_packages: Option<u64>,

/// The Unix timestamp in milliseconds of the oldest record in this
/// revision.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub oldest: Option<TimestampMs>,

/// The Unix timestamp in milliseconds of the newest record in this
/// revision.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub newest: Option<TimestampMs>,
}

/// A revision paired with its metadata: the flattened form of a
/// [`RepodataRevisions`] entry, used as indexer input and in reporter messages.
#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, Clone)]
pub struct RepodataRevisionInfo {
/// The integer identifying the revision.
Expand All @@ -130,6 +155,27 @@ pub struct RepodataRevisionInfo {
pub newest: Option<TimestampMs>,
}

impl RepodataRevisionInfo {
/// Combines a revision identifier with its metadata.
pub fn from_metadata(revision: RepodataRevision, metadata: RepodataRevisionMetadata) -> Self {
Self {
revision,
n_packages: metadata.n_packages,
oldest: metadata.oldest,
newest: metadata.newest,
}
}

/// Returns the metadata portion (everything but the revision).
pub fn metadata(&self) -> RepodataRevisionMetadata {
RepodataRevisionMetadata {
n_packages: self.n_packages,
oldest: self.oldest,
newest: self.newest,
}
}
}

/// A repodata revision.
///
/// The serialized CEP wire format is an integer. Known variants are exposed as
Expand Down Expand Up @@ -1121,7 +1167,7 @@ mod test {
info: Some(ChannelInfo {
subdir: Some("linux-64".to_string()),
base_url: None,
repodata_revisions: Vec::new(),
repodata_revisions: IndexMap::default(),
channel_relations: Some(ChannelRelations {
base: Some("../conda-forge".to_string()),
overrides: None,
Expand All @@ -1146,7 +1192,7 @@ mod test {
info: Some(ChannelInfo {
subdir: Some("linux-64".to_string()),
base_url: None,
repodata_revisions: Vec::new(),
repodata_revisions: IndexMap::default(),
channel_relations,
}),
packages: IndexMap::default(),
Expand All @@ -1164,36 +1210,38 @@ mod test {
let raw = r#"{
"info": {
"subdir": "linux-64",
"repodata_revisions": [
{
"revision": 4,
"repodata_revisions": {
"v4": {
"n_packages": 2,
"oldest": 1768249989851,
"newest": 1773851561010
}
]
}
},
"packages": {},
"packages.conda": {}
}"#;

let repodata: RepoData = serde_json::from_str(raw).unwrap();
let revision = &repodata.info.as_ref().unwrap().repodata_revisions[0];
assert_eq!(revision.revision, RepodataRevision::Unknown(4));
assert_eq!(revision.n_packages, Some(2));
let revisions = &repodata.info.as_ref().unwrap().repodata_revisions;
assert_eq!(revisions.len(), 1);
let metadata = &revisions[&RepodataRevision::Unknown(4)];
assert_eq!(metadata.n_packages, Some(2));
assert_eq!(
revision.oldest.map(|ts| ts.timestamp_millis()),
metadata.oldest.map(|ts| ts.timestamp_millis()),
Some(1768249989851)
);
assert_eq!(
revision.newest.map(|ts| ts.timestamp_millis()),
metadata.newest.map(|ts| ts.timestamp_millis()),
Some(1773851561010)
);

let json = serde_json::to_string(&repodata).unwrap();
assert!(json.contains("\"repodata_revisions\""));
assert!(json.contains("\"repodata_revisions\":{\"v4\":{"));
assert!(json.contains("\"oldest\":1768249989851"));
assert!(json.contains("\"newest\":1773851561010"));
// The revision identifier is the map key, not a field of the value.
assert!(!json.contains("\"revision\""));
}

#[test]
Expand Down
14 changes: 9 additions & 5 deletions crates/rattler_conda_types/src/repo_data/sharded.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

use crate::PackageRecord;
use crate::package::DistArchiveIdentifier;
use crate::repo_data::{ChannelRelations, RepodataRevisionInfo, V3Packages};
use crate::repo_data::{ChannelRelations, RepodataRevisions, V3Packages};
use indexmap::IndexMap;
use jiff::Timestamp;
use rattler_digest::{Sha256, Sha256Hash, serde::SerializableHash};
use serde::{Deserialize, Serialize};
use serde_with::{DisplayFromStr, serde_as};

/// The sharded repodata holds a hashmap of package name -> shard (hash).
/// This index file is stored under
Expand All @@ -24,6 +25,7 @@ pub struct ShardedRepodata {

/// Information about a sharded subdirectory that is stored inside the index
/// file.
#[serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ShardedSubdirInfo {
/// The name of the subdirectory
Expand All @@ -47,9 +49,11 @@ pub struct ShardedSubdirInfo {

/// Repodata revisions available through this sharded index.
///
/// See <https://github.com/conda/ceps/pull/146>.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub repodata_revisions: Vec<RepodataRevisionInfo>,
/// Serialized as a `vN`-keyed dictionary per the CEP draft
/// <https://github.com/conda/ceps/pull/146>.
#[serde_as(as = "IndexMap<DisplayFromStr, _>")]
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub repodata_revisions: RepodataRevisions,

/// Optional relationships to other channels as defined in
/// [CEP-42](https://github.com/conda/ceps/blob/main/cep-0042.md).
Expand Down Expand Up @@ -86,7 +90,7 @@ mod tests {
base_url: "./".to_string(),
shards_base_url: "./shards/".to_string(),
created_at: None,
repodata_revisions: Vec::new(),
repodata_revisions: IndexMap::default(),
channel_relations,
};
let json = serde_json::to_string(&info).unwrap();
Expand Down
44 changes: 20 additions & 24 deletions crates/rattler_index/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ use rattler_conda_types::{
RunExportsJson, WheelArchiveType,
},
};
pub use rattler_conda_types::{RepodataRevision, RepodataRevisionInfo};
pub use rattler_conda_types::{
RepodataRevision, RepodataRevisionInfo, RepodataRevisionMetadata, RepodataRevisions,
};
pub use rattler_config::config::index::{
IndexChannelConfig, IndexConfig, PackageRevisionAssignment,
};
Expand Down Expand Up @@ -1081,48 +1083,42 @@ impl RevisionStats {
fn repodata_revisions_for_packages(
configured: &[RepodataRevisionInfo],
v3: &V3Packages,
) -> Vec<RepodataRevisionInfo> {
) -> RepodataRevisions {
// `BTreeMap` keeps the result ordered ascending regardless of input order.
let mut revisions = configured
.iter()
.filter(|revision| revision.revision != RepodataRevision::Legacy)
.map(|revision| (revision.revision, revision.clone()))
.collect::<BTreeMap<_, _>>();
.filter(|info| info.revision != RepodataRevision::Legacy)
.map(|info| (info.revision, info.metadata()))
.collect::<BTreeMap<_, RepodataRevisionMetadata>>();

let mut stats = BTreeMap::<RepodataRevision, RevisionStats>::new();
for (_, record) in v3.records() {
stats.entry(RepodataRevision::V3).or_default().add(record);
}

for (revision, revision_stats) in stats {
let info = revisions
.entry(revision)
.or_insert_with(|| RepodataRevisionInfo {
revision,
n_packages: None,
oldest: None,
newest: None,
});
if info.n_packages.is_none() {
info.n_packages = Some(revision_stats.n_packages);
let metadata = revisions.entry(revision).or_default();
if metadata.n_packages.is_none() {
metadata.n_packages = Some(revision_stats.n_packages);
}
if info.oldest.is_none() {
info.oldest = revision_stats.oldest;
if metadata.oldest.is_none() {
metadata.oldest = revision_stats.oldest;
}
if info.newest.is_none() {
info.newest = revision_stats.newest;
if metadata.newest.is_none() {
metadata.newest = revision_stats.newest;
}
}

// Currently only v3 package maps are supported, but keep configured
// revisions with zero packages so clients can still surface channel
// capability information.
for revision in revisions.values_mut() {
if revision.n_packages.is_none() {
revision.n_packages = Some(0);
for metadata in revisions.values_mut() {
if metadata.n_packages.is_none() {
metadata.n_packages = Some(0);
}
}

revisions.into_values().collect()
revisions.into_iter().collect()
}

/// Write a `repodata.json` for all packages in the given configurator's root.
Expand Down Expand Up @@ -1699,7 +1695,7 @@ pub async fn ensure_channel_initialized_with_channel_metadata(
info: Some(ChannelInfo {
subdir: Some(Platform::NoArch.to_string()),
base_url: channel_metadata.base_url,
repodata_revisions: Vec::new(),
repodata_revisions: RepodataRevisions::new(),
channel_relations: channel_metadata.channel_relations,
}),
packages: IndexMap::default(),
Expand Down
22 changes: 7 additions & 15 deletions crates/rattler_index/tests/integration/basic_indexing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -273,19 +273,17 @@ async fn test_index_latest_repodata_revision() {
.pointer("/v3/conda/empty-0.1.0-h4616a5c_0")
.is_some()
);
let revision = &repodata_json["info"]["repodata_revisions"][0];
assert_eq!(revision["revision"], 3);
let revision = &repodata_json["info"]["repodata_revisions"]["v3"];
assert_eq!(revision["n_packages"], 1);

let shard_index_bytes = fs::read(subdir_path.join("repodata_shards.msgpack.zst")).unwrap();
let shard_index_bytes = zstd::decode_all(shard_index_bytes.as_slice()).unwrap();
let shard_index: ShardedRepodata = rmp_serde::from_slice(&shard_index_bytes).unwrap();
assert_eq!(shard_index.info.repodata_revisions.len(), 1);
assert_eq!(
shard_index.info.repodata_revisions[0].revision,
RepodataRevision::V3
shard_index.info.repodata_revisions[&RepodataRevision::V3].n_packages,
Some(1)
);
assert_eq!(shard_index.info.repodata_revisions[0].n_packages, Some(1));
}

#[tokio::test]
Expand Down Expand Up @@ -374,8 +372,7 @@ async fn test_index_repodata_revision_from_index_json() {
.pointer("/v3/tar.bz2/revision-demo-1.0.0-h123_0")
.is_some()
);
let revision = &repodata_json["info"]["repodata_revisions"][0];
assert_eq!(revision["revision"], 3);
let revision = &repodata_json["info"]["repodata_revisions"]["v3"];
assert_eq!(revision["n_packages"], 1);
assert_eq!(revision["oldest"], 1710000000000i64);
assert_eq!(revision["newest"], 1710000000000i64);
Expand Down Expand Up @@ -429,11 +426,7 @@ async fn test_index_writes_channel_metadata() {
"../fallback"
);
assert_eq!(
repodata_json["info"]["repodata_revisions"][0]["revision"],
3
);
assert_eq!(
repodata_json["info"]["repodata_revisions"][0]["n_packages"],
repodata_json["info"]["repodata_revisions"]["v3"]["n_packages"],
0
);

Expand Down Expand Up @@ -462,8 +455,7 @@ async fn test_index_writes_channel_metadata() {
Some("../fallback")
);
assert_eq!(
shard_index.info.repodata_revisions[0].revision,
RepodataRevision::V3
shard_index.info.repodata_revisions[&RepodataRevision::V3].n_packages,
Some(0)
);
assert_eq!(shard_index.info.repodata_revisions[0].n_packages, Some(0));
}
4 changes: 2 additions & 2 deletions crates/rattler_repodata_gateway/src/gateway/local_subdir.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::{path::Path, sync::Arc};

use rattler_conda_types::{Channel, PackageName, RepodataRevisionInfo};
use rattler_conda_types::{Channel, PackageName, RepodataRevisions};

use crate::{
Reporter,
Expand Down Expand Up @@ -108,7 +108,7 @@ impl SubdirClient for LocalSubdirClient {
.collect()
}

fn repodata_revisions(&self) -> &[RepodataRevisionInfo] {
fn repodata_revisions(&self) -> &RepodataRevisions {
self.sparse.repodata_revisions()
}
}
Loading
Loading