Skip to main content

varve_core/
bazel.rs

1//! Bazel checksum-registry compilation (REQ-BAZEL-001).
2//!
3//! rules_wasm_component pins tools with per-tool JSON checksum registries
4//! and a pure sha256 download path. This module compiles those registries
5//! FROM a verified layer manifest: the hashes Bazel enforces become
6//! transcriptions from a signed, counter-protected document instead of
7//! trust-on-first-use hashes of whatever a release page served. Bazel's
8//! fetch path does not change; its trust anchor does.
9//!
10//! The digests exported are the SOURCE-ASSET digests (the bytes Bazel
11//! downloads), recorded inside the signed payload at deposit time — the
12//! layer's own entry digests cover the extracted binaries, which Bazel
13//! never sees.
14
15use 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
28/// Map a target triple to rules_wasm_component's platform-key vocabulary.
29pub 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/// One compiled registry per tool, plus what could not be compiled and why.
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub struct BazelExport {
43    /// tool name → registry JSON (rules_wasm_component schema).
44    pub registries: BTreeMap<String, serde_json::Value>,
45    /// (tool, platform, reason) skipped — loud, never silent.
46    pub skipped: Vec<(String, String, String)>,
47}
48
49/// Compile the registries from a (verified-by-the-caller) layer manifest.
50pub fn export(manifest: &LayerManifest) -> BazelExport {
51    let mut skipped = Vec::new();
52    // tool → version → bazel platform key → {sha256, url_suffix}
53    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    // rivet: verifies REQ-BAZEL-001
180    #[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        // sha256: prefix normalized to bare hex, per the rules schema.
197        assert_eq!(
198            platforms["linux_amd64"]["sha256"],
199            "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
200        );
201        // Provenance header names the layer and forbids hand-editing.
202        let header = rivet["_generated_by"].as_str().unwrap();
203        assert!(header.contains("2026.08.1") && header.contains("Do not hand-edit"));
204    }
205
206    // rivet: verifies REQ-BAZEL-001
207    #[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    // rivet: verifies REQ-BAZEL-001
222    #[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}