1use std::path::{Path, PathBuf};
9
10use crate::loader::{LeanLibraryDependency, LeanLoaderCheck, LeanLoaderDiagnosticCode, LeanLoaderReport};
11
12#[derive(Clone, Debug, Eq, PartialEq)]
19pub struct CapabilityManifest {
20 pub primary_dylib: PathBuf,
22 pub package: String,
24 pub module: String,
26 pub dependencies: Vec<LeanLibraryDependency>,
28 pub lean_version: Option<String>,
30 pub resolved_lean_version: Option<String>,
32 pub lean_header_sha256: Option<String>,
34}
35
36impl CapabilityManifest {
37 pub fn read(path: &Path) -> Result<Self, LeanLoaderCheck> {
43 let bytes = std::fs::read(path).map_err(|err| {
44 let code = if err.kind() == std::io::ErrorKind::NotFound {
45 LeanLoaderDiagnosticCode::MissingManifest
46 } else {
47 LeanLoaderDiagnosticCode::MalformedManifest
48 };
49 LeanLoaderCheck::error(
50 code,
51 path.display().to_string(),
52 format!("failed to read Lean capability manifest '{}': {err}", path.display()),
53 "rebuild the Lean capability through CargoLeanCapability and ensure the manifest file is packaged",
54 )
55 })?;
56 let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(|err| {
57 LeanLoaderCheck::error(
58 LeanLoaderDiagnosticCode::MalformedManifest,
59 path.display().to_string(),
60 format!("Lean capability manifest '{}' is not valid JSON: {err}", path.display()),
61 "rebuild the Lean capability through CargoLeanCapability",
62 )
63 })?;
64 let schema_version = required_u64(&value, "schema_version", path)?;
65 if schema_version != u64::from(crate::CAPABILITY_MANIFEST_SCHEMA_VERSION) {
66 return Err(LeanLoaderCheck::error(
67 LeanLoaderDiagnosticCode::UnsupportedManifestSchema,
68 path.display().to_string(),
69 format!(
70 "unsupported Lean capability manifest schema {schema_version}; supported schema is {}",
71 crate::CAPABILITY_MANIFEST_SCHEMA_VERSION
72 ),
73 "rebuild the Lean capability with the same lean-rs version as the runtime crate",
74 ));
75 }
76 let primary_dylib = PathBuf::from(required_string(&value, "primary_dylib", path)?);
77 let package = required_string(&value, "package", path)?;
78 let module = required_string(&value, "module", path)?;
79 let dependencies = dependencies_from_manifest(&value, path)?;
80 let fingerprint = value.get("toolchain_fingerprint").unwrap_or(&serde_json::Value::Null);
81 let lean_version =
82 optional_string(fingerprint, "lean_version").or_else(|| optional_string(&value, "lean_version"));
83 let resolved_lean_version = optional_string(fingerprint, "resolved_version")
84 .or_else(|| optional_string(&value, "resolved_lean_version"));
85 let lean_header_sha256 =
86 optional_string(fingerprint, "header_sha256").or_else(|| optional_string(&value, "lean_header_sha256"));
87 Ok(Self {
88 primary_dylib,
89 package,
90 module,
91 dependencies,
92 lean_version,
93 resolved_lean_version,
94 lean_header_sha256,
95 })
96 }
97}
98
99#[must_use]
106pub fn check_static(manifest_path: &Path) -> LeanLoaderReport {
107 let manifest = match CapabilityManifest::read(manifest_path) {
108 Ok(manifest) => manifest,
109 Err(check) => return LeanLoaderReport::new(Some(manifest_path.to_path_buf()), vec![check]),
110 };
111
112 let mut checks = Vec::new();
113 check_fingerprint(&manifest, &mut checks);
114 check_staleness(manifest_path, &manifest, &mut checks);
115 check_dependency_paths(&manifest, &mut checks);
116 check_primary_dylib_present(&manifest, &mut checks);
117
118 LeanLoaderReport::new(Some(manifest_path.to_path_buf()), checks)
119}
120
121pub fn check_fingerprint(manifest: &CapabilityManifest, checks: &mut Vec<LeanLoaderCheck>) {
123 if let Some(version) = manifest.lean_version.as_deref()
124 && lean_rs_sys::supported_for(version).is_none()
125 {
126 checks.push(LeanLoaderCheck::error(
127 LeanLoaderDiagnosticCode::UnsupportedToolchainFingerprint,
128 version,
129 format!("manifest was built with unsupported Lean toolchain {version}"),
130 "rebuild the Lean capability with a Lean version supported by this lean-rs release",
131 ));
132 return;
133 }
134 if let Some(digest) = manifest.lean_header_sha256.as_deref()
135 && digest != lean_rs_sys::LEAN_HEADER_DIGEST
136 {
137 checks.push(LeanLoaderCheck::error(
138 LeanLoaderDiagnosticCode::UnsupportedToolchainFingerprint,
139 digest,
140 format!(
141 "manifest Lean header digest {digest} does not match this process digest {}",
142 lean_rs_sys::LEAN_HEADER_DIGEST
143 ),
144 "rebuild the Lean capability with the same Lean toolchain used by this Rust binary",
145 ));
146 return;
147 }
148 if let Some(resolved) = manifest.resolved_lean_version.as_deref()
149 && resolved != lean_rs_sys::LEAN_RESOLVED_VERSION
150 {
151 checks.push(LeanLoaderCheck::error(
152 LeanLoaderDiagnosticCode::UnsupportedToolchainFingerprint,
153 resolved,
154 format!(
155 "manifest resolved Lean version {resolved} does not match this process resolved version {}",
156 lean_rs_sys::LEAN_RESOLVED_VERSION
157 ),
158 "rebuild the Lean capability with the same Lean toolchain used by this Rust binary",
159 ));
160 }
161}
162
163pub fn check_staleness(manifest_path: &Path, manifest: &CapabilityManifest, checks: &mut Vec<LeanLoaderCheck>) {
165 let Ok(manifest_modified) = std::fs::metadata(manifest_path).and_then(|metadata| metadata.modified()) else {
166 return;
167 };
168 let Ok(primary_modified) = std::fs::metadata(&manifest.primary_dylib).and_then(|metadata| metadata.modified())
169 else {
170 return;
171 };
172 if primary_modified > manifest_modified {
173 checks.push(LeanLoaderCheck::error(
174 LeanLoaderDiagnosticCode::StaleManifest,
175 manifest_path.display().to_string(),
176 format!(
177 "manifest '{}' is older than primary dylib '{}'",
178 manifest_path.display(),
179 manifest.primary_dylib.display()
180 ),
181 "rebuild the Lean capability through CargoLeanCapability so the manifest matches the dylib",
182 ));
183 }
184}
185
186fn check_dependency_paths(manifest: &CapabilityManifest, checks: &mut Vec<LeanLoaderCheck>) {
187 for dependency in &manifest.dependencies {
188 if !dependency.path_ref().is_file() {
189 checks.push(LeanLoaderCheck::error(
190 LeanLoaderDiagnosticCode::MissingTransitiveDependency,
191 dependency.path_ref().display().to_string(),
192 format!(
193 "Lean dependency dylib '{}' named by the manifest is missing",
194 dependency.path_ref().display()
195 ),
196 "rebuild the Lean capability through CargoLeanCapability and package every manifest dependency",
197 ));
198 }
199 }
200}
201
202fn check_primary_dylib_present(manifest: &CapabilityManifest, checks: &mut Vec<LeanLoaderCheck>) {
203 if !manifest.primary_dylib.is_file() {
204 checks.push(LeanLoaderCheck::error(
205 LeanLoaderDiagnosticCode::MissingPrimaryDylib,
206 manifest.primary_dylib.display().to_string(),
207 format!(
208 "primary Lean capability dylib '{}' named by the manifest is missing",
209 manifest.primary_dylib.display()
210 ),
211 "rebuild the Lean capability through CargoLeanCapability and package the primary dylib",
212 ));
213 }
214}
215
216fn dependencies_from_manifest(
217 value: &serde_json::Value,
218 path: &Path,
219) -> Result<Vec<LeanLibraryDependency>, LeanLoaderCheck> {
220 let Some(raw_dependencies) = value.get("dependencies") else {
221 return Ok(Vec::new());
222 };
223 let dependencies = raw_dependencies.as_array().ok_or_else(|| {
224 LeanLoaderCheck::error(
225 LeanLoaderDiagnosticCode::MalformedManifest,
226 path.display().to_string(),
227 format!(
228 "Lean capability manifest '{}' field `dependencies` must be an array",
229 path.display()
230 ),
231 "rebuild the Lean capability through CargoLeanCapability",
232 )
233 })?;
234 let mut out = Vec::with_capacity(dependencies.len());
235 for dependency in dependencies {
236 let dylib = required_string(dependency, "dylib_path", path)?;
237 let mut descriptor = LeanLibraryDependency::path(dylib);
238 if dependency
239 .get("export_symbols_for_dependents")
240 .and_then(serde_json::Value::as_bool)
241 .unwrap_or(false)
242 {
243 descriptor = descriptor.export_symbols_for_dependents();
244 }
245 if let Some(initializer) = dependency.get("initializer") {
246 let package = required_string(initializer, "package", path)?;
247 let module = required_string(initializer, "module", path)?;
248 descriptor = descriptor.initializer(package, module);
249 }
250 out.push(descriptor);
251 }
252 Ok(out)
253}
254
255fn required_string(value: &serde_json::Value, field: &str, path: &Path) -> Result<String, LeanLoaderCheck> {
256 value
257 .get(field)
258 .and_then(serde_json::Value::as_str)
259 .filter(|value| !value.is_empty())
260 .map(str::to_owned)
261 .ok_or_else(|| {
262 LeanLoaderCheck::error(
263 LeanLoaderDiagnosticCode::MalformedManifest,
264 path.display().to_string(),
265 format!(
266 "Lean capability manifest '{}' is missing non-empty string field `{field}`",
267 path.display()
268 ),
269 "rebuild the Lean capability through CargoLeanCapability",
270 )
271 })
272}
273
274fn optional_string(value: &serde_json::Value, field: &str) -> Option<String> {
275 value
276 .get(field)
277 .and_then(serde_json::Value::as_str)
278 .filter(|value| !value.is_empty())
279 .map(str::to_owned)
280}
281
282fn required_u64(value: &serde_json::Value, field: &str, path: &Path) -> Result<u64, LeanLoaderCheck> {
283 value.get(field).and_then(serde_json::Value::as_u64).ok_or_else(|| {
284 LeanLoaderCheck::error(
285 LeanLoaderDiagnosticCode::MalformedManifest,
286 path.display().to_string(),
287 format!(
288 "Lean capability manifest '{}' is missing unsigned integer field `{field}`",
289 path.display()
290 ),
291 "rebuild the Lean capability through CargoLeanCapability",
292 )
293 })
294}