Skip to content
Draft
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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/rattler_conda_types/src/minimal_prefix_record.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ impl MinimalPrefixRecord {
constrains: Vec::new(),
features: None,
flags: Vec::new(),
indexed_timestamp: None,
legacy_bz2_size: None,
license: None,
license_family: None,
Expand Down
47 changes: 47 additions & 0 deletions crates/rattler_conda_types/src/repo_data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -446,6 +446,11 @@ pub struct PackageRecord {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub flags: Vec<Flag>,

/// The time at which an indexing tool first added this artifact to the
/// channel index. Set by indexing tools, never by build tools.
/// See the draft CEP: <https://github.com/conda/ceps/pull/154>.
pub indexed_timestamp: Option<crate::utils::TimestampMs>,

/// A deprecated md5 hash
#[serde_as(as = "Option<SerializableHash::<rattler_digest::Md5>>")]
pub legacy_bz2_md5: Option<Md5Hash>,
Expand Down Expand Up @@ -790,6 +795,7 @@ impl PackageRecord {
depends: vec![],
features: None,
flags: vec![],
indexed_timestamp: None,
legacy_bz2_md5: None,
legacy_bz2_size: None,
license: None,
Expand Down Expand Up @@ -1051,6 +1057,7 @@ impl PackageRecord {
depends: index.depends,
features: index.features,
flags: index.flags,
indexed_timestamp: None,
legacy_bz2_md5: None,
legacy_bz2_size: None,
license: index.license,
Expand Down Expand Up @@ -1137,6 +1144,46 @@ mod test {
insta::assert_snapshot!(json);
}

// See the draft CEP: https://github.com/conda/ceps/pull/154
#[test]
fn test_indexed_timestamp_roundtrip() {
// A record with `indexed_timestamp` round-trips through JSON.
let raw = r#"{
"build": "h123_0",
"build_number": 0,
"depends": [],
"indexed_timestamp": 1650000000000,
"name": "demo",
"subdir": "noarch",
"timestamp": 1640000000000,
"version": "1.0.0"
}"#;
let record: PackageRecord = serde_json::from_str(raw).unwrap();
assert_eq!(
record
.indexed_timestamp
.map(|timestamp| timestamp.timestamp_millis()),
Some(1_650_000_000_000)
);
let json = serde_json::to_value(&record).unwrap();
assert_eq!(json["indexed_timestamp"], 1_650_000_000_000_i64);

// A record without the field omits it on serialization.
let record: PackageRecord = serde_json::from_str(
r#"{
"build": "h123_0",
"build_number": 0,
"name": "demo",
"subdir": "noarch",
"version": "1.0.0"
}"#,
)
.unwrap();
assert_eq!(record.indexed_timestamp, None);
let json = serde_json::to_value(&record).unwrap();
assert!(json.get("indexed_timestamp").is_none());
}

// See https://github.com/conda/ceps/blob/main/cep-0042.md
#[test]
fn test_channel_relations() {
Expand Down
1 change: 1 addition & 0 deletions crates/rattler_config/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ indexmap = { workspace = true }
rattler_conda_types = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
strum = { workspace = true, features = ["derive"] }
toml = { workspace = true }
tracing = { workspace = true }
url = { workspace = true, features = ["serde"] }
Expand Down
54 changes: 54 additions & 0 deletions crates/rattler_config/src/config/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,35 @@ impl PackageRevisionAssignment {
}
}

/// How `indexed_timestamp` is backfilled for records in existing repodata
/// that lack the field. Newly indexed packages always get the current
/// indexing time, regardless of this setting.
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Default,
Deserialize,
Serialize,
strum::Display,
strum::EnumString,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
pub enum BackfillIndexedTimestamps {
/// Seed missing values from the package's build `timestamp` (clamped to
/// the indexing time), falling back to the indexing time when the package
/// has no `timestamp`.
#[default]
FromCondaPackageTimestamp,
/// Seed missing values with the indexing time.
Now,
/// Leave records without an `indexed_timestamp` untouched.
Off,
}

/// Index options that apply to a single channel.
///
/// Every field is optional so that `IndexConfig` can layer multiple entries
Expand Down Expand Up @@ -90,6 +119,11 @@ pub struct IndexChannelConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub package_revision_assignment: Option<PackageRevisionAssignment>,

/// How `indexed_timestamp` is backfilled for records in existing repodata
/// that lack the field.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub backfill_indexed_timestamps: Option<BackfillIndexedTimestamps>,

/// `info.base_url` value written to generated repodata.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub base_url: Option<String>,
Expand All @@ -106,6 +140,7 @@ impl IndexChannelConfig {
&& self.write_shards.is_none()
&& self.repodata_revisions.is_none()
&& self.package_revision_assignment.is_none()
&& self.backfill_indexed_timestamps.is_none()
&& self.base_url.is_none()
&& self.channel_relations.is_none()
}
Expand All @@ -121,6 +156,9 @@ impl IndexChannelConfig {
package_revision_assignment: other
.package_revision_assignment
.or(self.package_revision_assignment),
backfill_indexed_timestamps: other
.backfill_indexed_timestamps
.or(self.backfill_indexed_timestamps),
base_url: other.base_url.or_else(|| self.base_url.clone()),
channel_relations: other
.channel_relations
Expand Down Expand Up @@ -419,6 +457,22 @@ base-url = "../packages/"
assert!(resolved.base_url.is_none());
}

#[test]
fn parses_backfill_indexed_timestamps() {
let cfg = parse("backfill-indexed-timestamps = \"off\"\n");
assert_eq!(
cfg.default.backfill_indexed_timestamps,
Some(BackfillIndexedTimestamps::Off)
);
assert_eq!(
"from-conda-package-timestamp"
.parse::<BackfillIndexedTimestamps>()
.unwrap(),
BackfillIndexedTimestamps::FromCondaPackageTimestamp
);
assert!("invalid".parse::<BackfillIndexedTimestamps>().is_err());
}

#[test]
fn rejects_numeric_repodata_revisions() {
let err = toml::from_str::<IndexConfig>("repodata-revisions = [3]\n").unwrap_err();
Expand Down
11 changes: 10 additions & 1 deletion crates/rattler_index/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,15 @@ configures S3 credentials, concurrency, and per-channel index options under the

When `--config` is omitted, `rattler-index` falls back to its built-in defaults
(`write-zst = true`, `write-shards = true`, no advertised repodata revisions,
`from-index-json` revision assignment, no channel metadata).
`from-index-json` revision assignment, `from-conda-package-timestamp` backfill
of `indexed_timestamp`, no channel metadata).

Newly indexed packages are stamped with an `indexed_timestamp` — the time at
which the indexing run added them to the channel index (see the
[draft CEP](https://github.com/conda/ceps/pull/154)). Once
assigned, the value is preserved across re-indexing runs, including runs with
`--force`. Packages whose build `timestamp` lies in the future of the indexing
time are rejected and fail the indexing run.

## Per-channel index configuration

Expand Down Expand Up @@ -78,6 +86,7 @@ Matching rules:
| `write-shards` | boolean | Writes `repodata_shards.msgpack.zst` and shard files. Defaults to `true`. |
| `repodata-revisions` | array | Repodata revisions to enable. Each entry is a string (`"v3"`, `"legacy"`). The indexer fills revision package counts and timestamps while writing repodata. |
| `package-revision-assignment` | string | Controls which `repodata-revisions` bucket a freshly indexed package lands in. `from-index-json` (default) reads the revision from each package's `info/index.json`, so legacy packages stay in the legacy maps and v3-tagged packages go to the v3 bucket. `latest` is an opt-in override that forces every package into the newest configured revision — useful for migrating a whole channel onto v3 in one shot. A future revision-assignment mode will pick based on a package's timestamp so repodata can be deterministically recreated. |
| `backfill-indexed-timestamps` | string | Controls how `indexed_timestamp` is backfilled for records in existing repodata that lack the field. `from-conda-package-timestamp` (default) seeds it from the package's build `timestamp` (clamped so it never exceeds the indexing time), falling back to the indexing time when the package has no `timestamp`. `now` seeds it with the indexing time. `off` leaves records without the field untouched. Newly indexed packages always get the current indexing time, and previously assigned values are never recomputed. Can be overridden on the command line with `--backfill-indexed-timestamps`. |
| `base-url` | string | Writes `info.base_url` in generated `repodata.json` and sharded repodata metadata. May be relative or absolute. |
| `channel-relations.base` | string | A single channel reference with higher priority than this channel, written to `info.channel_relations.base`. |
| `channel-relations.overrides` | string | A single channel reference with lower priority than this channel, written to `info.channel_relations.overrides`. |
Expand Down
13 changes: 13 additions & 0 deletions crates/rattler_index/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ pub enum RepodataError {
#[error(transparent)]
Join(#[from] tokio::task::JoinError),

/// A package has a build timestamp in the future.
#[error(
"package {filename} has a build timestamp ({timestamp}) in the future of the indexing time ({indexing_time}); refusing to index"
)]
InvalidTimestamp {
/// The filename of the offending package.
filename: String,
/// The build timestamp of the package.
timestamp: jiff::Timestamp,
/// The time at which the indexing run started.
indexing_time: jiff::Timestamp,
},

/// A generic error.
#[error(transparent)]
Other(#[from] anyhow::Error),
Expand Down
Loading
Loading