1use std::collections::HashSet;
10use std::path::{Path, PathBuf};
11
12use object::{Object, ObjectSymbol};
13
14use super::initializer::InitializerName;
15use super::{LeanBuiltCapability, LeanLibraryDependency};
16use crate::error::{LeanError, bound_message};
17
18#[non_exhaustive]
20#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
21pub enum LeanLoaderDiagnosticCode {
22 MissingManifest,
24 MalformedManifest,
26 UnsupportedManifestSchema,
28 MissingPrimaryDylib,
30 MissingTransitiveDependency,
32 UnsupportedArchitecture,
34 UnsupportedToolchainFingerprint,
36 StaleManifest,
38 MissingInitializer,
40 MissingImportedSymbol,
42}
43
44impl LeanLoaderDiagnosticCode {
45 #[must_use]
47 pub const fn as_str(self) -> &'static str {
48 match self {
49 Self::MissingManifest => "lean_rs.loader.missing_manifest",
50 Self::MalformedManifest => "lean_rs.loader.malformed_manifest",
51 Self::UnsupportedManifestSchema => "lean_rs.loader.unsupported_manifest_schema",
52 Self::MissingPrimaryDylib => "lean_rs.loader.missing_primary_dylib",
53 Self::MissingTransitiveDependency => "lean_rs.loader.missing_transitive_dependency",
54 Self::UnsupportedArchitecture => "lean_rs.loader.unsupported_architecture",
55 Self::UnsupportedToolchainFingerprint => "lean_rs.loader.unsupported_toolchain_fingerprint",
56 Self::StaleManifest => "lean_rs.loader.stale_manifest",
57 Self::MissingInitializer => "lean_rs.loader.missing_initializer",
58 Self::MissingImportedSymbol => "lean_rs.loader.missing_imported_symbol",
59 }
60 }
61}
62
63impl std::fmt::Display for LeanLoaderDiagnosticCode {
64 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65 f.write_str(self.as_str())
66 }
67}
68
69#[non_exhaustive]
71#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
72pub enum LeanLoaderSeverity {
73 Info,
75 Warning,
77 Error,
79}
80
81#[derive(Clone, Debug, Eq, PartialEq)]
83pub struct LeanLoaderCheck {
84 code: LeanLoaderDiagnosticCode,
85 severity: LeanLoaderSeverity,
86 subject: String,
87 message: String,
88 repair_hint: String,
89}
90
91impl LeanLoaderCheck {
92 fn error(
93 code: LeanLoaderDiagnosticCode,
94 subject: impl Into<String>,
95 message: impl Into<String>,
96 repair_hint: impl Into<String>,
97 ) -> Self {
98 Self {
99 code,
100 severity: LeanLoaderSeverity::Error,
101 subject: bound_message(subject.into()),
102 message: bound_message(message.into()),
103 repair_hint: bound_message(repair_hint.into()),
104 }
105 }
106
107 #[must_use]
109 pub fn code(&self) -> LeanLoaderDiagnosticCode {
110 self.code
111 }
112
113 #[must_use]
115 pub fn severity(&self) -> LeanLoaderSeverity {
116 self.severity
117 }
118
119 #[must_use]
121 pub fn subject(&self) -> &str {
122 &self.subject
123 }
124
125 #[must_use]
127 pub fn message(&self) -> &str {
128 &self.message
129 }
130
131 #[must_use]
133 pub fn repair_hint(&self) -> &str {
134 &self.repair_hint
135 }
136}
137
138impl std::fmt::Display for LeanLoaderCheck {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 write!(
141 f,
142 "{} [{:?}] {}: {} (repair: {})",
143 self.code.as_str(),
144 self.severity,
145 self.subject,
146 self.message,
147 self.repair_hint
148 )
149 }
150}
151
152#[derive(Clone, Debug, Eq, PartialEq)]
154pub struct LeanLoaderReport {
155 manifest_path: Option<PathBuf>,
156 checks: Vec<LeanLoaderCheck>,
157}
158
159impl LeanLoaderReport {
160 fn new(manifest_path: Option<PathBuf>, checks: Vec<LeanLoaderCheck>) -> Self {
161 Self { manifest_path, checks }
162 }
163
164 #[must_use]
166 pub fn manifest_path(&self) -> Option<&Path> {
167 self.manifest_path.as_deref()
168 }
169
170 #[must_use]
172 pub fn checks(&self) -> &[LeanLoaderCheck] {
173 &self.checks
174 }
175
176 pub fn errors(&self) -> impl Iterator<Item = &LeanLoaderCheck> {
178 self.checks
179 .iter()
180 .filter(|check| check.severity == LeanLoaderSeverity::Error)
181 }
182
183 #[must_use]
185 pub fn is_ok(&self) -> bool {
186 self.errors().next().is_none()
187 }
188
189 #[must_use]
191 pub fn first_error(&self) -> Option<&LeanLoaderCheck> {
192 self.errors().next()
193 }
194
195 pub(crate) fn into_error(self) -> LeanError {
196 let Some(first) = self
197 .checks
198 .into_iter()
199 .find(|check| check.severity == LeanLoaderSeverity::Error)
200 else {
201 return LeanError::module_init("Lean capability preflight failed without a recorded finding");
202 };
203 LeanError::module_init(format!(
204 "{}: {}. repair: {}",
205 first.code.as_str(),
206 first.message,
207 first.repair_hint
208 ))
209 }
210}
211
212#[derive(Clone, Debug, Eq, PartialEq)]
214pub struct LeanCapabilityPreflight {
215 spec: LeanBuiltCapability,
216}
217
218impl LeanCapabilityPreflight {
219 #[must_use]
221 pub fn new(spec: LeanBuiltCapability) -> Self {
222 Self { spec }
223 }
224
225 #[must_use]
227 pub fn check(&self) -> LeanLoaderReport {
228 let manifest_path = match self.spec.resolved_manifest_path() {
229 Ok(path) => path,
230 Err(err) => {
231 return LeanLoaderReport::new(
232 None,
233 vec![LeanLoaderCheck::error(
234 LeanLoaderDiagnosticCode::MissingManifest,
235 "manifest",
236 err.to_string(),
237 "rebuild the Lean capability through CargoLeanCapability and embed the manifest env var",
238 )],
239 );
240 }
241 };
242
243 let manifest = match CapabilityManifest::read(&manifest_path) {
244 Ok(manifest) => manifest,
245 Err(check) => return LeanLoaderReport::new(Some(manifest_path), vec![check]),
246 };
247
248 let mut checks = Vec::new();
249 check_fingerprint(&manifest, &mut checks);
250 check_staleness(&manifest_path, &manifest, &mut checks);
251
252 let mut dependency_exports = HashSet::new();
253 for dependency in &manifest.dependencies {
254 match inspect_artifact(dependency.path_ref(), ArtifactRole::Dependency) {
255 Ok(info) => {
256 dependency_exports.extend(info.defined_symbols);
257 }
258 Err(check) => checks.push(check),
259 }
260 }
261
262 match inspect_artifact(&manifest.primary_dylib, ArtifactRole::Primary) {
263 Ok(info) => {
264 check_initializer(&manifest, &info, &mut checks);
265 check_imported_symbols(&manifest, &info, &dependency_exports, &mut checks);
266 }
267 Err(check) => checks.push(check),
268 }
269
270 LeanLoaderReport::new(Some(manifest_path), checks)
271 }
272}
273
274#[derive(Clone, Debug, Eq, PartialEq)]
275pub(crate) struct CapabilityManifest {
276 pub(crate) primary_dylib: PathBuf,
277 pub(crate) package: String,
278 pub(crate) module: String,
279 pub(crate) dependencies: Vec<LeanLibraryDependency>,
280 pub(crate) lean_version: Option<String>,
281 pub(crate) resolved_lean_version: Option<String>,
282 pub(crate) lean_header_sha256: Option<String>,
283}
284
285impl CapabilityManifest {
286 pub(crate) fn read(path: &Path) -> Result<Self, LeanLoaderCheck> {
287 let bytes = std::fs::read(path).map_err(|err| {
288 let code = if err.kind() == std::io::ErrorKind::NotFound {
289 LeanLoaderDiagnosticCode::MissingManifest
290 } else {
291 LeanLoaderDiagnosticCode::MalformedManifest
292 };
293 LeanLoaderCheck::error(
294 code,
295 path.display().to_string(),
296 format!("failed to read Lean capability manifest '{}': {err}", path.display()),
297 "rebuild the Lean capability through CargoLeanCapability and ensure the manifest file is packaged",
298 )
299 })?;
300 let value: serde_json::Value = serde_json::from_slice(&bytes).map_err(|err| {
301 LeanLoaderCheck::error(
302 LeanLoaderDiagnosticCode::MalformedManifest,
303 path.display().to_string(),
304 format!("Lean capability manifest '{}' is not valid JSON: {err}", path.display()),
305 "rebuild the Lean capability through CargoLeanCapability",
306 )
307 })?;
308 let schema_version = required_u64(&value, "schema_version", path)?;
309 if schema_version != u64::from(lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION) {
310 return Err(LeanLoaderCheck::error(
311 LeanLoaderDiagnosticCode::UnsupportedManifestSchema,
312 path.display().to_string(),
313 format!(
314 "unsupported Lean capability manifest schema {schema_version}; supported schema is {}",
315 lean_toolchain::CAPABILITY_MANIFEST_SCHEMA_VERSION
316 ),
317 "rebuild the Lean capability with the same lean-rs version as the runtime crate",
318 ));
319 }
320 let primary_dylib = PathBuf::from(required_string(&value, "primary_dylib", path)?);
321 let package = required_string(&value, "package", path)?;
322 let module = required_string(&value, "module", path)?;
323 let dependencies = dependencies_from_manifest(&value, path)?;
324 let fingerprint = value.get("toolchain_fingerprint").unwrap_or(&serde_json::Value::Null);
325 let lean_version =
326 optional_string(fingerprint, "lean_version").or_else(|| optional_string(&value, "lean_version"));
327 let resolved_lean_version = optional_string(fingerprint, "resolved_version")
328 .or_else(|| optional_string(&value, "resolved_lean_version"));
329 let lean_header_sha256 =
330 optional_string(fingerprint, "header_sha256").or_else(|| optional_string(&value, "lean_header_sha256"));
331 Ok(Self {
332 primary_dylib,
333 package,
334 module,
335 dependencies,
336 lean_version,
337 resolved_lean_version,
338 lean_header_sha256,
339 })
340 }
341}
342
343#[derive(Clone, Debug)]
344struct ArtifactInfo {
345 defined_symbols: HashSet<String>,
346 undefined_symbols: HashSet<String>,
347}
348
349#[derive(Copy, Clone, Debug, Eq, PartialEq)]
350enum ArtifactRole {
351 Primary,
352 Dependency,
353}
354
355fn inspect_artifact(path: &Path, role: ArtifactRole) -> Result<ArtifactInfo, LeanLoaderCheck> {
356 let missing_code = match role {
357 ArtifactRole::Primary => LeanLoaderDiagnosticCode::MissingPrimaryDylib,
358 ArtifactRole::Dependency => LeanLoaderDiagnosticCode::MissingTransitiveDependency,
359 };
360 let repair = match role {
361 ArtifactRole::Primary => {
362 "rebuild the Lean capability through CargoLeanCapability and package the primary dylib"
363 }
364 ArtifactRole::Dependency => {
365 "rebuild the Lean capability through CargoLeanCapability and package every manifest dependency"
366 }
367 };
368 let bytes = std::fs::read(path).map_err(|err| {
369 let code = if err.kind() == std::io::ErrorKind::NotFound {
370 missing_code
371 } else {
372 LeanLoaderDiagnosticCode::UnsupportedArchitecture
373 };
374 LeanLoaderCheck::error(
375 code,
376 path.display().to_string(),
377 format!("failed to read Lean dylib '{}': {err}", path.display()),
378 repair,
379 )
380 })?;
381 let file = object::File::parse(&*bytes).map_err(|err| {
382 LeanLoaderCheck::error(
383 LeanLoaderDiagnosticCode::UnsupportedArchitecture,
384 path.display().to_string(),
385 format!(
386 "Lean dylib '{}' is not a supported native object for this platform: {err}",
387 path.display()
388 ),
389 "rebuild the Lean capability for the current target architecture",
390 )
391 })?;
392 if !architecture_matches_host(file.architecture()) {
393 return Err(LeanLoaderCheck::error(
394 LeanLoaderDiagnosticCode::UnsupportedArchitecture,
395 path.display().to_string(),
396 format!(
397 "Lean dylib '{}' has architecture {:?}, but this process is {}",
398 path.display(),
399 file.architecture(),
400 std::env::consts::ARCH
401 ),
402 "rebuild the Lean capability for the current target architecture",
403 ));
404 }
405 let strip_underscore = matches!(file.format(), object::BinaryFormat::MachO | object::BinaryFormat::Wasm);
406 let mut defined_symbols = HashSet::new();
407 let mut undefined_symbols = HashSet::new();
408 for symbol in file.symbols().chain(file.dynamic_symbols()) {
409 let Ok(name) = symbol.name() else {
410 continue;
411 };
412 let normalised = normalise_symbol_name(name, strip_underscore);
413 if normalised.is_empty() {
414 continue;
415 }
416 if symbol.is_undefined() {
417 undefined_symbols.insert(normalised.to_owned());
418 } else if symbol.is_global() {
419 defined_symbols.insert(normalised.to_owned());
420 }
421 }
422 Ok(ArtifactInfo {
423 defined_symbols,
424 undefined_symbols,
425 })
426}
427
428fn check_initializer(manifest: &CapabilityManifest, primary: &ArtifactInfo, checks: &mut Vec<LeanLoaderCheck>) {
429 let initializer = match InitializerName::from_lake_names(&manifest.package, &manifest.module) {
430 Ok(initializer) => initializer,
431 Err(err) => {
432 checks.push(LeanLoaderCheck::error(
433 LeanLoaderDiagnosticCode::MalformedManifest,
434 format!("{}/{}", manifest.package, manifest.module),
435 err.to_string(),
436 "rebuild the manifest with valid Lake package and module names",
437 ));
438 return;
439 }
440 };
441 if primary.defined_symbols.contains(initializer.symbol_str())
442 || primary.defined_symbols.contains(initializer.legacy_symbol_str())
443 {
444 return;
445 }
446 checks.push(LeanLoaderCheck::error(
447 LeanLoaderDiagnosticCode::MissingInitializer,
448 format!("{}/{}", manifest.package, manifest.module),
449 format!(
450 "primary dylib '{}' does not export initializer '{}' or '{}'",
451 manifest.primary_dylib.display(),
452 initializer.symbol_str(),
453 initializer.legacy_symbol_str()
454 ),
455 "check the package/module names and rebuild the Lean capability shared target",
456 ));
457}
458
459fn check_imported_symbols(
460 manifest: &CapabilityManifest,
461 primary: &ArtifactInfo,
462 dependency_exports: &HashSet<String>,
463 checks: &mut Vec<LeanLoaderCheck>,
464) {
465 for symbol in primary
466 .undefined_symbols
467 .iter()
468 .filter(|symbol| is_lean_dependency_symbol(symbol))
469 {
470 if dependency_exports.contains(symbol) {
471 continue;
472 }
473 checks.push(LeanLoaderCheck::error(
474 LeanLoaderDiagnosticCode::MissingImportedSymbol,
475 symbol.clone(),
476 format!(
477 "primary dylib '{}' imports Lean symbol '{symbol}' that is not provided by the manifest dependencies",
478 manifest.primary_dylib.display()
479 ),
480 "rebuild the Lean capability through CargoLeanCapability so dependency dylibs are recorded in the manifest",
481 ));
482 return;
483 }
484}
485
486fn check_fingerprint(manifest: &CapabilityManifest, checks: &mut Vec<LeanLoaderCheck>) {
487 if let Some(version) = manifest.lean_version.as_deref()
488 && lean_rs_sys::supported_for(version).is_none()
489 {
490 checks.push(LeanLoaderCheck::error(
491 LeanLoaderDiagnosticCode::UnsupportedToolchainFingerprint,
492 version,
493 format!("manifest was built with unsupported Lean toolchain {version}"),
494 "rebuild the Lean capability with a Lean version supported by this lean-rs release",
495 ));
496 return;
497 }
498 if let Some(digest) = manifest.lean_header_sha256.as_deref()
499 && digest != lean_rs_sys::LEAN_HEADER_DIGEST
500 {
501 checks.push(LeanLoaderCheck::error(
502 LeanLoaderDiagnosticCode::UnsupportedToolchainFingerprint,
503 digest,
504 format!(
505 "manifest Lean header digest {digest} does not match this process digest {}",
506 lean_rs_sys::LEAN_HEADER_DIGEST
507 ),
508 "rebuild the Lean capability with the same Lean toolchain used by this Rust binary",
509 ));
510 return;
511 }
512 if let Some(resolved) = manifest.resolved_lean_version.as_deref()
513 && resolved != lean_rs_sys::LEAN_RESOLVED_VERSION
514 {
515 checks.push(LeanLoaderCheck::error(
516 LeanLoaderDiagnosticCode::UnsupportedToolchainFingerprint,
517 resolved,
518 format!(
519 "manifest resolved Lean version {resolved} does not match this process resolved version {}",
520 lean_rs_sys::LEAN_RESOLVED_VERSION
521 ),
522 "rebuild the Lean capability with the same Lean toolchain used by this Rust binary",
523 ));
524 }
525}
526
527fn check_staleness(manifest_path: &Path, manifest: &CapabilityManifest, checks: &mut Vec<LeanLoaderCheck>) {
528 let Ok(manifest_modified) = std::fs::metadata(manifest_path).and_then(|metadata| metadata.modified()) else {
529 return;
530 };
531 let Ok(primary_modified) = std::fs::metadata(&manifest.primary_dylib).and_then(|metadata| metadata.modified())
532 else {
533 return;
534 };
535 if primary_modified > manifest_modified {
536 checks.push(LeanLoaderCheck::error(
537 LeanLoaderDiagnosticCode::StaleManifest,
538 manifest_path.display().to_string(),
539 format!(
540 "manifest '{}' is older than primary dylib '{}'",
541 manifest_path.display(),
542 manifest.primary_dylib.display()
543 ),
544 "rebuild the Lean capability through CargoLeanCapability so the manifest matches the dylib",
545 ));
546 }
547}
548
549fn dependencies_from_manifest(
550 value: &serde_json::Value,
551 path: &Path,
552) -> Result<Vec<LeanLibraryDependency>, LeanLoaderCheck> {
553 let Some(raw_dependencies) = value.get("dependencies") else {
554 return Ok(Vec::new());
555 };
556 let dependencies = raw_dependencies.as_array().ok_or_else(|| {
557 LeanLoaderCheck::error(
558 LeanLoaderDiagnosticCode::MalformedManifest,
559 path.display().to_string(),
560 format!(
561 "Lean capability manifest '{}' field `dependencies` must be an array",
562 path.display()
563 ),
564 "rebuild the Lean capability through CargoLeanCapability",
565 )
566 })?;
567 let mut out = Vec::with_capacity(dependencies.len());
568 for dependency in dependencies {
569 let dylib = required_string(dependency, "dylib_path", path)?;
570 let mut descriptor = LeanLibraryDependency::path(dylib);
571 if dependency
572 .get("export_symbols_for_dependents")
573 .and_then(serde_json::Value::as_bool)
574 .unwrap_or(false)
575 {
576 descriptor = descriptor.export_symbols_for_dependents();
577 }
578 if let Some(initializer) = dependency.get("initializer") {
579 let package = required_string(initializer, "package", path)?;
580 let module = required_string(initializer, "module", path)?;
581 descriptor = descriptor.initializer(package, module);
582 }
583 out.push(descriptor);
584 }
585 Ok(out)
586}
587
588fn required_string(value: &serde_json::Value, field: &str, path: &Path) -> Result<String, LeanLoaderCheck> {
589 value
590 .get(field)
591 .and_then(serde_json::Value::as_str)
592 .filter(|value| !value.is_empty())
593 .map(str::to_owned)
594 .ok_or_else(|| {
595 LeanLoaderCheck::error(
596 LeanLoaderDiagnosticCode::MalformedManifest,
597 path.display().to_string(),
598 format!(
599 "Lean capability manifest '{}' is missing non-empty string field `{field}`",
600 path.display()
601 ),
602 "rebuild the Lean capability through CargoLeanCapability",
603 )
604 })
605}
606
607fn optional_string(value: &serde_json::Value, field: &str) -> Option<String> {
608 value
609 .get(field)
610 .and_then(serde_json::Value::as_str)
611 .filter(|value| !value.is_empty())
612 .map(str::to_owned)
613}
614
615fn required_u64(value: &serde_json::Value, field: &str, path: &Path) -> Result<u64, LeanLoaderCheck> {
616 value.get(field).and_then(serde_json::Value::as_u64).ok_or_else(|| {
617 LeanLoaderCheck::error(
618 LeanLoaderDiagnosticCode::MalformedManifest,
619 path.display().to_string(),
620 format!(
621 "Lean capability manifest '{}' is missing unsigned integer field `{field}`",
622 path.display()
623 ),
624 "rebuild the Lean capability through CargoLeanCapability",
625 )
626 })
627}
628
629fn architecture_matches_host(architecture: object::Architecture) -> bool {
630 matches!(
631 (std::env::consts::ARCH, architecture),
632 ("x86_64", object::Architecture::X86_64)
633 | ("aarch64", object::Architecture::Aarch64)
634 | ("arm", object::Architecture::Arm)
635 | ("x86", object::Architecture::I386)
636 )
637}
638
639fn normalise_symbol_name(name: &str, strip_underscore: bool) -> &str {
640 if strip_underscore {
641 name.strip_prefix('_').unwrap_or(name)
642 } else {
643 name
644 }
645}
646
647fn is_lean_dependency_symbol(symbol: &str) -> bool {
648 symbol.starts_with("LeanRs") || symbol.starts_with("lean_rs_") || symbol.starts_with("initialize_LeanRs")
649}
650
651pub(crate) fn manifest_error_to_lean_error(check: LeanLoaderCheck) -> LeanError {
652 LeanLoaderReport::new(None, vec![check]).into_error()
653}
654
655#[cfg(test)]
656#[allow(clippy::expect_used, clippy::panic)]
657mod tests {
658 use super::{
659 ArtifactInfo, CapabilityManifest, LeanCapabilityPreflight, LeanLoaderDiagnosticCode, check_imported_symbols,
660 check_staleness, inspect_artifact,
661 };
662 use crate::{LeanBuiltCapability, LeanCapability, LeanRuntime};
663 use std::collections::HashSet;
664 use std::fs;
665 use std::path::{Path, PathBuf};
666 use std::time::Duration;
667
668 #[test]
669 fn missing_manifest_reports_stable_code() {
670 let path = temp_dir("missing_manifest").join("missing.json");
671 let report = LeanCapabilityPreflight::new(LeanBuiltCapability::manifest_path(path)).check();
672
673 assert!(!report.is_ok());
674 assert_eq!(
675 report.first_error().map(crate::LeanLoaderCheck::code),
676 Some(LeanLoaderDiagnosticCode::MissingManifest)
677 );
678 }
679
680 #[test]
681 fn malformed_manifest_reports_stable_code() {
682 let dir = temp_dir("malformed_manifest");
683 let manifest = dir.join("capability.json");
684 fs::write(&manifest, "{").expect("write malformed manifest");
685
686 let report = LeanCapabilityPreflight::new(LeanBuiltCapability::manifest_path(&manifest)).check();
687
688 assert_eq!(
689 report.first_error().map(crate::LeanLoaderCheck::code),
690 Some(LeanLoaderDiagnosticCode::MalformedManifest)
691 );
692 assert_eq!(report.manifest_path(), Some(manifest.as_path()));
693 }
694
695 #[test]
696 fn unsupported_manifest_schema_reports_stable_code() {
697 let dir = temp_dir("unsupported_manifest_schema");
698 let manifest = dir.join("capability.json");
699 fs::write(
700 &manifest,
701 r#"{
702 "schema_version": 999,
703 "target_name": "Cap",
704 "package": "pkg",
705 "module": "Cap",
706 "primary_dylib": "/tmp/libcap.so"
707}"#,
708 )
709 .expect("write unsupported manifest schema");
710
711 let report = LeanCapabilityPreflight::new(LeanBuiltCapability::manifest_path(manifest)).check();
712
713 assert_eq!(
714 report.first_error().map(crate::LeanLoaderCheck::code),
715 Some(LeanLoaderDiagnosticCode::UnsupportedManifestSchema)
716 );
717 }
718
719 #[test]
720 fn missing_primary_dylib_reports_stable_code() {
721 let dir = temp_dir("missing_primary_dylib");
722 let missing = dir.join("missing-capability.dylib");
723 let manifest = write_manifest(&dir, &missing, "", "");
724
725 let report = LeanCapabilityPreflight::new(LeanBuiltCapability::manifest_path(manifest)).check();
726
727 assert!(contains_code(&report, LeanLoaderDiagnosticCode::MissingPrimaryDylib));
728 }
729
730 #[test]
731 fn missing_dependency_reports_stable_code() {
732 let dir = temp_dir("missing_dependency");
733 let missing = dir.join("missing-dependency.dylib");
734 let dependency = format!(
735 r#""dependencies":[{{"dylib_path":"{}","export_symbols_for_dependents":true}}],"#,
736 json_path(&missing)
737 );
738 let manifest = write_manifest(&dir, current_exe(), &dependency, "");
739
740 let report = LeanCapabilityPreflight::new(LeanBuiltCapability::manifest_path(manifest)).check();
741
742 assert!(contains_code(
743 &report,
744 LeanLoaderDiagnosticCode::MissingTransitiveDependency
745 ));
746 }
747
748 #[test]
749 fn invalid_object_reports_unsupported_architecture() {
750 let dir = temp_dir("invalid_object");
751 let bad = dir.join("not-a-dylib.so");
752 fs::write(&bad, b"not an object").expect("write invalid object");
753
754 let err = inspect_artifact(&bad, super::ArtifactRole::Primary).expect_err("invalid object should fail");
755
756 assert_eq!(err.code(), LeanLoaderDiagnosticCode::UnsupportedArchitecture);
757 }
758
759 #[test]
760 fn unsupported_toolchain_fingerprint_reports_stable_code() {
761 let dir = temp_dir("unsupported_toolchain_fingerprint");
762 let manifest = dir.join("capability.json");
763 fs::write(
764 &manifest,
765 format!(
766 r#"{{
767 "schema_version": 1,
768 "target_name": "Cap",
769 "package": "pkg",
770 "module": "Cap",
771 "primary_dylib": "{}",
772 "toolchain_fingerprint": {{
773 "lean_version": "0.0.0",
774 "resolved_version": "0.0.0",
775 "header_sha256": "0000"
776 }}
777}}"#,
778 json_path(¤t_exe())
779 ),
780 )
781 .expect("write unsupported fingerprint manifest");
782
783 let report = LeanCapabilityPreflight::new(LeanBuiltCapability::manifest_path(manifest)).check();
784
785 assert!(contains_code(
786 &report,
787 LeanLoaderDiagnosticCode::UnsupportedToolchainFingerprint
788 ));
789 }
790
791 #[test]
792 fn stale_manifest_reports_stable_code() {
793 let dir = temp_dir("stale_manifest");
794 let primary = dir.join("libcap.so");
795 fs::write(&primary, b"old").expect("write old primary");
796 let manifest = write_manifest(&dir, &primary, "", "");
797 std::thread::sleep(Duration::from_millis(20));
798 fs::write(&primary, b"new").expect("write newer primary");
799 let parsed = CapabilityManifest::read(&manifest).expect("manifest parses");
800 let mut checks = Vec::new();
801
802 check_staleness(&manifest, &parsed, &mut checks);
803
804 assert!(
805 checks
806 .iter()
807 .any(|check| check.code() == LeanLoaderDiagnosticCode::StaleManifest)
808 );
809 }
810
811 #[test]
812 fn missing_initializer_reports_stable_code() {
813 let dir = temp_dir("missing_initializer");
814 let manifest = write_manifest(&dir, current_exe(), "", "");
815
816 let report = LeanCapabilityPreflight::new(LeanBuiltCapability::manifest_path(manifest)).check();
817
818 assert!(contains_code(&report, LeanLoaderDiagnosticCode::MissingInitializer));
819 }
820
821 #[test]
822 fn missing_imported_symbol_reports_stable_code() {
823 let primary = ArtifactInfo {
824 defined_symbols: HashSet::new(),
825 undefined_symbols: HashSet::from(["LeanRsInterop_missing".to_owned()]),
826 };
827 let manifest = CapabilityManifest {
828 primary_dylib: PathBuf::from("/tmp/libcap.so"),
829 package: "pkg".to_owned(),
830 module: "Cap".to_owned(),
831 dependencies: Vec::new(),
832 lean_version: None,
833 resolved_lean_version: None,
834 lean_header_sha256: None,
835 };
836 let mut checks = Vec::new();
837
838 check_imported_symbols(&manifest, &primary, &HashSet::new(), &mut checks);
839
840 assert!(
841 checks
842 .iter()
843 .any(|check| check.code() == LeanLoaderDiagnosticCode::MissingImportedSymbol)
844 );
845 }
846
847 #[test]
848 fn open_failure_uses_preflight_code_in_error_message() {
849 let dir = temp_dir("open_failure_preflight_code");
850 let missing = dir.join("missing-capability.dylib");
851 let manifest = write_manifest(&dir, &missing, "", "");
852 let runtime = LeanRuntime::init().expect("runtime init");
853
854 let Err(err) = LeanCapability::from_build_manifest(runtime, LeanBuiltCapability::manifest_path(manifest))
855 else {
856 panic!("missing primary should fail before open");
857 };
858
859 assert_eq!(err.code(), crate::LeanDiagnosticCode::ModuleInit);
860 assert!(
861 err.to_string()
862 .contains(LeanLoaderDiagnosticCode::MissingPrimaryDylib.as_str())
863 );
864 }
865
866 fn contains_code(report: &crate::LeanLoaderReport, code: LeanLoaderDiagnosticCode) -> bool {
867 report.checks().iter().any(|check| check.code() == code)
868 }
869
870 fn temp_dir(name: &str) -> PathBuf {
871 let dir = std::env::temp_dir().join(format!("lean-rs-preflight-{}-{name}", std::process::id()));
872 drop(fs::remove_dir_all(&dir));
873 fs::create_dir_all(&dir).expect("create preflight test dir");
874 dir
875 }
876
877 fn current_exe() -> PathBuf {
878 std::env::current_exe().expect("current test executable path")
879 }
880
881 fn write_manifest(dir: &Path, primary: impl AsRef<Path>, dependencies: &str, extra: &str) -> PathBuf {
882 let manifest = dir.join("capability.json");
883 let contents = format!(
884 r#"{{
885 {extra}
886 "schema_version": 1,
887 "target_name": "Cap",
888 "package": "pkg",
889 "module": "Cap",
890 "primary_dylib": "{}",
891 {dependencies}
892 "toolchain_fingerprint": {{
893 "lean_version": "{}",
894 "resolved_version": "{}",
895 "header_sha256": "{}"
896 }}
897}}"#,
898 json_path(primary.as_ref()),
899 lean_rs_sys::LEAN_VERSION,
900 lean_rs_sys::LEAN_RESOLVED_VERSION,
901 lean_rs_sys::LEAN_HEADER_DIGEST,
902 );
903 fs::write(&manifest, contents).expect("write manifest");
904 manifest
905 }
906
907 fn json_path(path: &Path) -> String {
908 path.display().to_string().replace('\\', "\\\\")
909 }
910}