1use std::collections::BTreeMap;
16
17use crate::manifest::LayerManifest;
18
19pub const ANN_SRC_REPO: &str = "eu.pulseengine.source.repo";
20pub const ANN_SRC_RELEASE: &str = "eu.pulseengine.source.release";
21pub const ANN_SRC_ASSET: &str = "eu.pulseengine.source.asset";
22pub const ANN_SRC_SHA256: &str = "eu.pulseengine.source.sha256";
23
24pub const ANN_RUNNER: &str = "eu.pulseengine.runner";
25pub const ANN_RUNNER_ARGS: &str = "eu.pulseengine.runner-args";
26pub const ANN_RUNNER_ARG_PREFIX: &str = "eu.pulseengine.runner-arg-prefix";
27
28pub fn bazel_platform_key(triple: &str) -> Option<&'static str> {
30 match triple {
31 "aarch64-apple-darwin" => Some("darwin_arm64"),
32 "x86_64-apple-darwin" => Some("darwin_amd64"),
33 "aarch64-unknown-linux-gnu" => Some("linux_arm64"),
34 "x86_64-unknown-linux-gnu" => Some("linux_amd64"),
35 "x86_64-pc-windows-msvc" => Some("windows_amd64"),
36 _ => None,
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct BazelExport {
43 pub registries: BTreeMap<String, serde_json::Value>,
45 pub skipped: Vec<(String, String, String)>,
47}
48
49pub fn export(manifest: &LayerManifest) -> BazelExport {
51 let mut skipped = Vec::new();
52 let mut tools: BTreeMap<String, (String, String, BTreeMap<String, serde_json::Value>)> =
54 BTreeMap::new();
55
56 for entry in &manifest.entries {
57 let ann = &entry.annotations;
58 let Some(tool) = ann.get("eu.pulseengine.tool") else {
59 continue;
60 };
61 let version = ann
62 .get("eu.pulseengine.tool.version")
63 .cloned()
64 .unwrap_or_default();
65 let platform = ann
66 .get(crate::platform::ANN_PLATFORM)
67 .cloned()
68 .unwrap_or_default();
69 let Some(key) = bazel_platform_key(&platform) else {
70 skipped.push((
71 tool.clone(),
72 platform.clone(),
73 "platform has no Bazel key".into(),
74 ));
75 continue;
76 };
77 let (Some(repo), Some(asset), Some(src_sha)) = (
78 ann.get(ANN_SRC_REPO),
79 ann.get(ANN_SRC_ASSET),
80 ann.get(ANN_SRC_SHA256),
81 ) else {
82 skipped.push((
83 tool.clone(),
84 platform.clone(),
85 "no source provenance recorded at deposit".into(),
86 ));
87 continue;
88 };
89 let hex = src_sha
90 .strip_prefix("sha256:")
91 .unwrap_or(src_sha)
92 .to_string();
93 let slot = tools
94 .entry(tool.clone())
95 .or_insert_with(|| (repo.clone(), version.clone(), BTreeMap::new()));
96 slot.2.insert(
97 key.to_string(),
98 serde_json::json!({ "sha256": hex, "url_suffix": asset }),
99 );
100 }
101
102 let registries = tools
103 .into_iter()
104 .map(|(tool, (repo, version, platforms))| {
105 let json = serde_json::json!({
106 "_generated_by": format!(
116 "varve export-bazel — layer {} (counter {}); digests transcribed from the \
117 signed layer manifest. The source-asset sha256 is transcribed from the \
118 deposit spec and is NOT verified by varve against the asset it names. \
119 Do not hand-edit.",
120 manifest.layer, manifest.counter
121 ),
122 "tool_name": tool,
123 "github_repo": repo,
124 "latest_version": version,
125 "versions": {
126 version.clone(): { "platforms": platforms }
127 }
128 });
129 (tool, json)
130 })
131 .collect();
132 BazelExport {
133 registries,
134 skipped,
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 fn manifest_with_sources() -> LayerManifest {
143 let payload = r#"{
144 "schemaVersion": 2,
145 "mediaType": "application/vnd.oci.image.index.v1+json",
146 "annotations": {
147 "eu.pulseengine.varve.layer": "2026.08.1",
148 "eu.pulseengine.varve.channel": "rolling",
149 "eu.pulseengine.varve.counter": "2",
150 "org.opencontainers.image.created": "2026-08-07T00:00:00Z"
151 },
152 "manifests": [
153 {
154 "digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
155 "annotations": {
156 "eu.pulseengine.tool": "rivet",
157 "eu.pulseengine.tool.version": "0.32.0",
158 "eu.pulseengine.platform": "aarch64-apple-darwin",
159 "eu.pulseengine.source.repo": "pulseengine/rivet",
160 "eu.pulseengine.source.release": "v0.32.0",
161 "eu.pulseengine.source.asset": "rivet-v0.32.0-aarch64-apple-darwin.tar.gz",
162 "eu.pulseengine.source.sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
163 }
164 },
165 {
166 "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
167 "annotations": {
168 "eu.pulseengine.tool": "rivet",
169 "eu.pulseengine.tool.version": "0.32.0",
170 "eu.pulseengine.platform": "x86_64-unknown-linux-gnu",
171 "eu.pulseengine.source.repo": "pulseengine/rivet",
172 "eu.pulseengine.source.release": "v0.32.0",
173 "eu.pulseengine.source.asset": "rivet-v0.32.0-x86_64-unknown-linux-gnu.tar.gz",
174 "eu.pulseengine.source.sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
175 }
176 },
177 {
178 "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
179 "annotations": {
180 "eu.pulseengine.tool": "wsc",
181 "eu.pulseengine.tool.version": "0.10.0",
182 "eu.pulseengine.platform": "aarch64-apple-darwin"
183 }
184 }
185 ]
186}"#;
187 LayerManifest::parse(payload.as_bytes()).unwrap()
188 }
189
190 #[test]
192 fn registries_compile_in_the_rules_schema_with_source_digests() {
193 let export = export(&manifest_with_sources());
194 let rivet = &export.registries["rivet"];
195 assert_eq!(rivet["tool_name"], "rivet");
196 assert_eq!(rivet["github_repo"], "pulseengine/rivet");
197 assert_eq!(rivet["latest_version"], "0.32.0");
198 let platforms = &rivet["versions"]["0.32.0"]["platforms"];
199 assert_eq!(
200 platforms["darwin_arm64"]["sha256"],
201 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
202 );
203 assert_eq!(
204 platforms["darwin_arm64"]["url_suffix"],
205 "rivet-v0.32.0-aarch64-apple-darwin.tar.gz"
206 );
207 assert_eq!(
209 platforms["linux_amd64"]["sha256"],
210 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
211 );
212 let header = rivet["_generated_by"].as_str().unwrap();
214 assert!(header.contains("2026.08.1") && header.contains("Do not hand-edit"));
215 assert!(
223 header.contains("NOT verified by varve"),
224 "the header must not claim a check varve does not perform: {header}"
225 );
226 }
227
228 #[test]
230 fn a_tool_without_source_provenance_is_skipped_loudly() {
231 let export = export(&manifest_with_sources());
232 assert!(!export.registries.contains_key("wsc"));
233 assert!(
234 export
235 .skipped
236 .iter()
237 .any(|(tool, _, reason)| tool == "wsc" && reason.contains("no source provenance")),
238 "skips must be reported: {:?}",
239 export.skipped
240 );
241 }
242
243 #[test]
245 fn platform_keys_map_the_rules_vocabulary() {
246 assert_eq!(
247 bazel_platform_key("aarch64-apple-darwin"),
248 Some("darwin_arm64")
249 );
250 assert_eq!(
251 bazel_platform_key("x86_64-apple-darwin"),
252 Some("darwin_amd64")
253 );
254 assert_eq!(
255 bazel_platform_key("aarch64-unknown-linux-gnu"),
256 Some("linux_arm64")
257 );
258 assert_eq!(
259 bazel_platform_key("x86_64-unknown-linux-gnu"),
260 Some("linux_amd64")
261 );
262 assert_eq!(
263 bazel_platform_key("x86_64-pc-windows-msvc"),
264 Some("windows_amd64")
265 );
266 assert_eq!(bazel_platform_key("wasm32-wasip2"), None);
267 }
268}