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