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!(
107 "varve export-bazel — layer {} (counter {}); digests transcribed from the \
108 signed layer manifest. Do not hand-edit.",
109 manifest.layer, manifest.counter
110 ),
111 "tool_name": tool,
112 "github_repo": repo,
113 "latest_version": version,
114 "versions": {
115 version.clone(): { "platforms": platforms }
116 }
117 });
118 (tool, json)
119 })
120 .collect();
121 BazelExport {
122 registries,
123 skipped,
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 fn manifest_with_sources() -> LayerManifest {
132 let payload = r#"{
133 "schemaVersion": 2,
134 "mediaType": "application/vnd.oci.image.index.v1+json",
135 "annotations": {
136 "eu.pulseengine.varve.layer": "2026.08.1",
137 "eu.pulseengine.varve.channel": "rolling",
138 "eu.pulseengine.varve.counter": "2",
139 "org.opencontainers.image.created": "2026-08-07T00:00:00Z"
140 },
141 "manifests": [
142 {
143 "digest": "sha256:1111111111111111111111111111111111111111111111111111111111111111",
144 "annotations": {
145 "eu.pulseengine.tool": "rivet",
146 "eu.pulseengine.tool.version": "0.32.0",
147 "eu.pulseengine.platform": "aarch64-apple-darwin",
148 "eu.pulseengine.source.repo": "pulseengine/rivet",
149 "eu.pulseengine.source.release": "v0.32.0",
150 "eu.pulseengine.source.asset": "rivet-v0.32.0-aarch64-apple-darwin.tar.gz",
151 "eu.pulseengine.source.sha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
152 }
153 },
154 {
155 "digest": "sha256:2222222222222222222222222222222222222222222222222222222222222222",
156 "annotations": {
157 "eu.pulseengine.tool": "rivet",
158 "eu.pulseengine.tool.version": "0.32.0",
159 "eu.pulseengine.platform": "x86_64-unknown-linux-gnu",
160 "eu.pulseengine.source.repo": "pulseengine/rivet",
161 "eu.pulseengine.source.release": "v0.32.0",
162 "eu.pulseengine.source.asset": "rivet-v0.32.0-x86_64-unknown-linux-gnu.tar.gz",
163 "eu.pulseengine.source.sha256": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
164 }
165 },
166 {
167 "digest": "sha256:3333333333333333333333333333333333333333333333333333333333333333",
168 "annotations": {
169 "eu.pulseengine.tool": "wsc",
170 "eu.pulseengine.tool.version": "0.10.0",
171 "eu.pulseengine.platform": "aarch64-apple-darwin"
172 }
173 }
174 ]
175}"#;
176 LayerManifest::parse(payload.as_bytes()).unwrap()
177 }
178
179 #[test]
181 fn registries_compile_in_the_rules_schema_with_source_digests() {
182 let export = export(&manifest_with_sources());
183 let rivet = &export.registries["rivet"];
184 assert_eq!(rivet["tool_name"], "rivet");
185 assert_eq!(rivet["github_repo"], "pulseengine/rivet");
186 assert_eq!(rivet["latest_version"], "0.32.0");
187 let platforms = &rivet["versions"]["0.32.0"]["platforms"];
188 assert_eq!(
189 platforms["darwin_arm64"]["sha256"],
190 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
191 );
192 assert_eq!(
193 platforms["darwin_arm64"]["url_suffix"],
194 "rivet-v0.32.0-aarch64-apple-darwin.tar.gz"
195 );
196 assert_eq!(
198 platforms["linux_amd64"]["sha256"],
199 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
200 );
201 let header = rivet["_generated_by"].as_str().unwrap();
203 assert!(header.contains("2026.08.1") && header.contains("Do not hand-edit"));
204 }
205
206 #[test]
208 fn a_tool_without_source_provenance_is_skipped_loudly() {
209 let export = export(&manifest_with_sources());
210 assert!(!export.registries.contains_key("wsc"));
211 assert!(
212 export
213 .skipped
214 .iter()
215 .any(|(tool, _, reason)| tool == "wsc" && reason.contains("no source provenance")),
216 "skips must be reported: {:?}",
217 export.skipped
218 );
219 }
220
221 #[test]
223 fn platform_keys_map_the_rules_vocabulary() {
224 assert_eq!(
225 bazel_platform_key("aarch64-apple-darwin"),
226 Some("darwin_arm64")
227 );
228 assert_eq!(
229 bazel_platform_key("x86_64-apple-darwin"),
230 Some("darwin_amd64")
231 );
232 assert_eq!(
233 bazel_platform_key("aarch64-unknown-linux-gnu"),
234 Some("linux_arm64")
235 );
236 assert_eq!(
237 bazel_platform_key("x86_64-unknown-linux-gnu"),
238 Some("linux_amd64")
239 );
240 assert_eq!(
241 bazel_platform_key("x86_64-pc-windows-msvc"),
242 Some("windows_amd64")
243 );
244 assert_eq!(bazel_platform_key("wasm32-wasip2"), None);
245 }
246}