1use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct InfoReport {
13 pub info_schema_version: u32,
14 pub runner_version: String,
15 pub wasmtime_version: String,
16 pub target_triple: String,
17 pub build_profile: String,
18 pub build_timestamp_utc: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub git_sha: Option<String>,
21 pub pack_format_versions: Vec<u32>,
22 pub features: Features,
23 pub wasi_imports: Vec<InterfaceBinding>,
24 pub greentic_imports: Vec<InterfaceBinding>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Features {
30 pub enabled: Vec<String>,
31 pub disabled: Vec<String>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct InterfaceBinding {
37 pub interface: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub version: Option<String>,
40 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub versions: Vec<String>,
42 #[serde(default, skip_serializing_if = "is_false")]
43 pub opt_in_per_pack: bool,
44}
45
46fn is_false(b: &bool) -> bool {
47 !*b
48}
49
50pub const PACK_FORMAT_VERSIONS: &[u32] = &[greentic_pack::builder::PACK_VERSION];
54
55pub const WASI_IMPORTS: &[(&str, &str)] = &[
58 ("wasi:io", "0.2.0"),
59 ("wasi:clocks", "0.2.0"),
60 ("wasi:filesystem", "0.2.0"),
61 ("wasi:random", "0.2.0"),
62 ("wasi:sockets", "0.2.0"),
63 ("wasi:http", "0.2.0"),
64 ("wasi:tls", "0.2.0"),
65];
66
67pub struct GreenticImport {
69 pub interface: &'static str,
70 pub versions: &'static [&'static str],
71 pub opt_in_per_pack: bool,
72}
73
74pub const GREENTIC_IMPORTS: &[GreenticImport] = &[
78 GreenticImport {
79 interface: "greentic:component/control",
80 versions: &["0.4.0", "0.5.0"],
81 opt_in_per_pack: false,
82 },
83 GreenticImport {
84 interface: "greentic:http/client",
85 versions: &["1.0.0", "1.1.0"],
86 opt_in_per_pack: false,
87 },
88 GreenticImport {
89 interface: "greentic:interfaces/host",
90 versions: &["0.6.0"],
91 opt_in_per_pack: false,
92 },
93 GreenticImport {
94 interface: "greentic:interfaces/telemetry",
95 versions: &["0.6.0"],
96 opt_in_per_pack: false,
97 },
98 GreenticImport {
99 interface: "greentic:interfaces/state-store",
100 versions: &["0.6.0"],
101 opt_in_per_pack: true,
102 },
103 GreenticImport {
104 interface: "greentic:interfaces/secrets",
105 versions: &["1.1.0"],
106 opt_in_per_pack: false,
107 },
108];
109
110pub fn collect() -> InfoReport {
112 let mut enabled = Vec::new();
113 let mut disabled = Vec::new();
114 for (feat, on) in [
115 ("verify", cfg!(feature = "verify")),
116 ("telemetry", true),
117 ("session-redis", cfg!(feature = "session-redis")),
118 ("fault-injection", cfg!(feature = "fault-injection")),
119 (
120 "component-v0-6-introspection",
121 cfg!(feature = "component-v0-6-introspection"),
122 ),
123 ] {
124 if on {
125 enabled.push(feat.to_string());
126 } else {
127 disabled.push(feat.to_string());
128 }
129 }
130
131 InfoReport {
132 info_schema_version: 1,
133 runner_version: env!("CARGO_PKG_VERSION").to_string(),
134 wasmtime_version: wasmtime_environ::VERSION.to_string(),
135 target_triple: env!("TARGET").to_string(),
136 build_profile: env!("BUILD_PROFILE").to_string(),
137 build_timestamp_utc: env!("BUILD_TIMESTAMP_UTC").to_string(),
138 git_sha: option_env!("GIT_SHA").map(String::from),
139 pack_format_versions: PACK_FORMAT_VERSIONS.to_vec(),
140 features: Features { enabled, disabled },
141 wasi_imports: WASI_IMPORTS
142 .iter()
143 .map(|(iface, ver)| InterfaceBinding {
144 interface: (*iface).to_string(),
145 version: Some((*ver).to_string()),
146 versions: Vec::new(),
147 opt_in_per_pack: false,
148 })
149 .collect(),
150 greentic_imports: GREENTIC_IMPORTS
151 .iter()
152 .map(|g| InterfaceBinding {
153 interface: g.interface.to_string(),
154 version: None,
155 versions: g.versions.iter().map(|v| (*v).to_string()).collect(),
156 opt_in_per_pack: g.opt_in_per_pack,
157 })
158 .collect(),
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165
166 #[test]
167 fn schema_version_is_one() {
168 let r = collect();
169 assert_eq!(r.info_schema_version, 1);
170 }
171
172 #[test]
173 fn features_are_disjoint() {
174 let r = collect();
175 for f in &r.features.enabled {
176 assert!(
177 !r.features.disabled.contains(f),
178 "feature {f} appears in both enabled and disabled"
179 );
180 }
181 }
182
183 #[test]
184 fn runner_version_is_non_empty() {
185 let r = collect();
186 assert!(!r.runner_version.is_empty());
187 assert!(!r.wasmtime_version.is_empty());
188 }
189
190 #[test]
196 fn greentic_imports_listed_here_are_registered() {
197 let path = concat!(
198 env!("CARGO_MANIFEST_DIR"),
199 "/../greentic-runner-host/src/pack.rs"
200 );
201 let source = std::fs::read_to_string(path).expect("read pack.rs");
202 for g in GREENTIC_IMPORTS {
203 let found_explicit = g
204 .versions
205 .iter()
206 .any(|v| source.contains(&format!("{}@{}", g.interface, v)));
207 let fallback = match g.interface {
213 "greentic:interfaces/host" => {
214 source.contains("runner_host_http") || source.contains("runner_host_kv")
215 }
216 "greentic:interfaces/telemetry" => source.contains("telemetry_logger"),
217 "greentic:interfaces/state-store" => source.contains("state_store"),
218 "greentic:interfaces/secrets" => source.contains("secrets_store_v1_1"),
219 _ => false,
220 };
221 assert!(
222 found_explicit || fallback,
223 "GREENTIC_IMPORTS entry {} not referenced in register_all() source — \
224 either the interface was renamed/removed upstream (update this list) \
225 or the test is looking at the wrong file.",
226 g.interface,
227 );
228 }
229 }
230}