1#![allow(unsafe_code)]
17
18use std::collections::HashMap;
19use std::path::Path;
20
21use super::preflight::{CapabilityManifest, LeanRuntimePreflight, manifest_error_to_lean_error, report_into_error};
22use super::{
23 DecodeCallResult, LeanArgs, LeanExported, LeanLibrary, LeanLibraryBundle, LeanLibraryDependency, LeanModule,
24};
25use crate::error::{LeanError, LeanResult};
26use crate::runtime::LeanRuntime;
27use lean_toolchain::{LeanExportSignature, LeanExportSymbolKind};
28
29pub use lean_toolchain::{BuiltCapabilityArtifact, LeanBuiltCapability, LeanBuiltCapabilityError};
34
35fn built_capability_error_to_lean_error(err: &LeanBuiltCapabilityError) -> LeanError {
36 LeanError::module_init(err.to_string())
37}
38
39pub struct LeanCapability<'lean> {
42 bundle: LeanLibraryBundle<'lean>,
43 package: String,
44 module: String,
45 export_signatures: HashMap<String, LeanExportSignature>,
46}
47
48#[derive(Debug)]
50pub enum LeanCheckedExportError {
51 MissingSignatureMetadata { symbol: String },
53 SignatureMismatch {
55 symbol: String,
56 expected: Box<LeanExportSignature>,
57 manifest: Box<LeanExportSignature>,
58 },
59 SymbolKindMismatch {
61 symbol: String,
62 manifest: LeanExportSymbolKind,
63 actual: LeanExportSymbolKind,
64 },
65 MissingSymbol { symbol: String, source: LeanError },
67 Module(LeanError),
69}
70
71impl std::fmt::Display for LeanCheckedExportError {
72 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
73 match self {
74 Self::MissingSignatureMetadata { symbol } => {
75 write!(f, "missing trusted export signature metadata for symbol '{symbol}'")
76 }
77 Self::SignatureMismatch {
78 symbol,
79 expected,
80 manifest,
81 } => write!(
82 f,
83 "export signature mismatch for symbol '{symbol}': requested {expected:?}, manifest has {manifest:?}"
84 ),
85 Self::SymbolKindMismatch {
86 symbol,
87 manifest,
88 actual,
89 } => write!(
90 f,
91 "export symbol kind mismatch for symbol '{symbol}': manifest has {manifest:?}, dylib has {actual:?}"
92 ),
93 Self::MissingSymbol { symbol, source } => {
94 write!(
95 f,
96 "manifest-backed export symbol '{symbol}' is missing from the loaded library: {source}"
97 )
98 }
99 Self::Module(err) => write!(f, "failed to initialize module before checked export lookup: {err}"),
100 }
101 }
102}
103
104impl std::error::Error for LeanCheckedExportError {
105 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
106 match self {
107 Self::MissingSymbol { source, .. } | Self::Module(source) => Some(source),
108 Self::MissingSignatureMetadata { .. }
109 | Self::SignatureMismatch { .. }
110 | Self::SymbolKindMismatch { .. } => None,
111 }
112 }
113}
114
115impl<'lean> LeanCapability<'lean> {
116 #[allow(clippy::needless_pass_by_value)]
125 pub fn from_build_manifest(runtime: &'lean LeanRuntime, spec: LeanBuiltCapability) -> LeanResult<Self> {
126 let report = LeanRuntimePreflight::new(spec.clone()).check();
127 if !report.is_ok() {
128 return Err(report_into_error(report));
129 }
130 let manifest_path = spec
131 .resolved_manifest_path()
132 .map_err(|err| built_capability_error_to_lean_error(&err))?;
133 let manifest = CapabilityManifest::read(&manifest_path).map_err(manifest_error_to_lean_error)?;
134 Self::open_with_dependencies_and_exports(
135 runtime,
136 manifest.primary_dylib,
137 manifest.package,
138 manifest.module,
139 manifest.dependencies,
140 manifest.exports,
141 )
142 }
143
144 pub fn from_build_env(runtime: &'lean LeanRuntime, mut spec: LeanBuiltCapability) -> LeanResult<Self> {
156 let dylib_path = spec
157 .dylib_path()
158 .map_err(|err| built_capability_error_to_lean_error(&err))?;
159 let package = spec.take_package_name().ok_or_else(|| {
160 LeanError::linking("LeanBuiltCapability is missing the Lake package name; call `.package(...)`")
161 })?;
162 let module = spec.take_module_name().ok_or_else(|| {
163 LeanError::linking("LeanBuiltCapability is missing the root Lean module name; call `.module(...)`")
164 })?;
165 let dependencies = spec.take_dependencies();
166 Self::open_with_dependencies_and_exports(runtime, dylib_path, package, module, dependencies, [])
167 }
168
169 pub fn open(
177 runtime: &'lean LeanRuntime,
178 dylib_path: impl AsRef<Path>,
179 package: impl Into<String>,
180 module: impl Into<String>,
181 ) -> LeanResult<Self> {
182 let package = package.into();
183 let module = module.into();
184 Self::open_with_dependencies_and_exports(runtime, dylib_path, package, module, [], [])
185 }
186
187 pub fn open_with_dependencies(
199 runtime: &'lean LeanRuntime,
200 dylib_path: impl AsRef<Path>,
201 package: impl Into<String>,
202 module: impl Into<String>,
203 dependencies: impl IntoIterator<Item = LeanLibraryDependency>,
204 ) -> LeanResult<Self> {
205 Self::open_with_dependencies_and_exports(runtime, dylib_path, package, module, dependencies, [])
206 }
207
208 fn open_with_dependencies_and_exports(
209 runtime: &'lean LeanRuntime,
210 dylib_path: impl AsRef<Path>,
211 package: impl Into<String>,
212 module: impl Into<String>,
213 dependencies: impl IntoIterator<Item = LeanLibraryDependency>,
214 export_signatures: impl IntoIterator<Item = LeanExportSignature>,
215 ) -> LeanResult<Self> {
216 let package = package.into();
217 let module = module.into();
218 let bundle = LeanLibraryBundle::open(runtime, dylib_path, dependencies)?;
219 let _module = bundle.initialize_module(&package, &module)?;
220 let export_signatures = export_signatures
221 .into_iter()
222 .map(|signature| (signature.symbol().to_owned(), signature))
223 .collect();
224 Ok(Self {
225 bundle,
226 package,
227 module,
228 export_signatures,
229 })
230 }
231
232 pub fn exported<Args, R>(&self, name: &str) -> Result<LeanExported<'lean, '_, Args, R>, LeanCheckedExportError>
245 where
246 Args: LeanArgs<'lean>,
247 R: DecodeCallResult<'lean>,
248 {
249 let manifest =
250 self.export_signatures
251 .get(name)
252 .ok_or_else(|| LeanCheckedExportError::MissingSignatureMetadata {
253 symbol: name.to_owned(),
254 })?;
255 let expected = match manifest.kind() {
256 LeanExportSymbolKind::Function => {
257 LeanExportSignature::function(name, Args::export_abi_args(), R::export_abi_return())
258 }
259 LeanExportSymbolKind::Global if Args::ARITY == 0 && !R::EXPECTS_IO_RESULT => {
260 LeanExportSignature::global(name, R::export_abi_return())
261 }
262 LeanExportSymbolKind::Global => {
263 LeanExportSignature::function(name, Args::export_abi_args(), R::export_abi_return())
264 }
265 };
266 if &expected != manifest {
267 return Err(LeanCheckedExportError::SignatureMismatch {
268 symbol: name.to_owned(),
269 expected: Box::new(expected),
270 manifest: Box::new(manifest.clone()),
271 });
272 }
273
274 let actual_kind = if self.library().globals().contains(name) {
275 LeanExportSymbolKind::Global
276 } else {
277 LeanExportSymbolKind::Function
278 };
279 if manifest.kind() != actual_kind {
280 return Err(LeanCheckedExportError::SymbolKindMismatch {
281 symbol: name.to_owned(),
282 manifest: manifest.kind(),
283 actual: actual_kind,
284 });
285 }
286
287 let module = self.module().map_err(LeanCheckedExportError::Module)?;
288 unsafe { module.exported_unchecked::<Args, R>(name) }.map_err(|source| LeanCheckedExportError::MissingSymbol {
292 symbol: name.to_owned(),
293 source,
294 })
295 }
296
297 pub fn module(&self) -> LeanResult<LeanModule<'lean, '_>> {
307 self.bundle.initialize_module(&self.package, &self.module)
308 }
309
310 #[must_use]
312 pub fn library(&self) -> &LeanLibrary<'lean> {
313 self.bundle.library()
314 }
315
316 #[must_use]
318 pub fn bundle(&self) -> &LeanLibraryBundle<'lean> {
319 &self.bundle
320 }
321
322 #[must_use]
324 pub fn package_name(&self) -> &str {
325 &self.package
326 }
327
328 #[must_use]
330 pub fn module_name(&self) -> &str {
331 &self.module
332 }
333
334 pub fn export_signatures(&self) -> impl Iterator<Item = &LeanExportSignature> {
336 self.export_signatures.values()
337 }
338}
339
340#[cfg(test)]
341#[allow(clippy::expect_used, clippy::panic)]
342mod tests {
343 use super::{
344 BuiltCapabilityArtifact, CapabilityManifest, LeanBuiltCapability, LeanBuiltCapabilityError,
345 LeanLibraryDependency,
346 };
347 use std::fs;
348 use std::path::PathBuf;
349
350 #[test]
351 fn built_capability_path_is_resolved_without_runtime_env() {
352 let spec = LeanBuiltCapability::path("/tmp/libcap.so")
353 .env_var("LEAN_RS_CAPABILITY_CAP_DYLIB")
354 .package("pkg")
355 .module("Cap");
356
357 let path = match spec.dylib_path() {
358 Ok(path) => path,
359 Err(err) => panic!("expected path, got {err}"),
360 };
361 assert_eq!(path, std::path::PathBuf::from("/tmp/libcap.so"));
362 assert_eq!(spec.package_name(), Some("pkg"));
363 assert_eq!(spec.module_name(), Some("Cap"));
364 }
365
366 #[test]
367 fn missing_runtime_env_is_typed() {
368 let spec = LeanBuiltCapability::env("LEAN_RS_TEST_MISSING_CAPABILITY_DYLIB")
369 .package("pkg")
370 .module("Cap");
371 let err = match spec.dylib_path() {
372 Ok(path) => panic!("expected missing env error, got {}", path.display()),
373 Err(err) => err,
374 };
375 assert!(matches!(
376 err,
377 LeanBuiltCapabilityError::EnvVarNotSet {
378 kind: BuiltCapabilityArtifact::Dylib,
379 ..
380 }
381 ));
382 }
383
384 #[test]
385 fn missing_runtime_manifest_env_is_typed() {
386 let spec = LeanBuiltCapability::manifest_env("LEAN_RS_TEST_MISSING_CAPABILITY_MANIFEST");
387 let err = match spec.resolved_manifest_path() {
388 Ok(path) => panic!("expected missing manifest env error, got {}", path.display()),
389 Err(err) => err,
390 };
391 assert!(matches!(
392 err,
393 LeanBuiltCapabilityError::EnvVarNotSet {
394 kind: BuiltCapabilityArtifact::Manifest,
395 ..
396 }
397 ));
398 }
399
400 #[test]
401 fn manifest_descriptor_parses_dependencies() {
402 let path = temp_manifest_path("manifest_descriptor_parses_dependencies");
403 write_manifest(
404 &path,
405 r#"{
406 "schema_version": 2,
407 "target_name": "Cap",
408 "package": "pkg",
409 "module": "Cap",
410 "primary_dylib": "/tmp/libcap.so",
411 "exports": [],
412 "dependencies": [
413 {
414 "dylib_path": "/tmp/libdep.so",
415 "export_symbols_for_dependents": true,
416 "initializer": { "package": "dep_pkg", "module": "Dep" }
417 }
418 ]
419}"#,
420 );
421
422 let manifest = match CapabilityManifest::read(&path) {
423 Ok(manifest) => manifest,
424 Err(err) => panic!("expected manifest to parse, got {err}"),
425 };
426 assert_eq!(manifest.primary_dylib, PathBuf::from("/tmp/libcap.so"));
427 assert_eq!(manifest.package, "pkg");
428 assert_eq!(manifest.module, "Cap");
429 assert_eq!(manifest.dependencies.len(), 1);
430 let Some(dependency) = manifest.dependencies.first() else {
431 panic!("expected one dependency");
432 };
433 assert!(dependency.exports_symbols_for_dependents());
434 assert_eq!(dependency.path_ref(), std::path::Path::new("/tmp/libdep.so"));
435 let Some(initializer) = dependency.module_initializer() else {
436 panic!("expected dependency initializer");
437 };
438 assert_eq!(initializer.package_name(), "dep_pkg");
439 assert_eq!(initializer.module_name(), "Dep");
440 }
441
442 #[test]
443 fn unsupported_manifest_schema_is_typed() {
444 let path = temp_manifest_path("unsupported_manifest_schema_is_typed");
445 write_manifest(
446 &path,
447 r#"{
448 "schema_version": 999,
449 "package": "pkg",
450 "module": "Cap",
451 "primary_dylib": "/tmp/libcap.so",
452 "exports": []
453}"#,
454 );
455
456 let Err(err) = CapabilityManifest::read(&path) else {
457 panic!("expected unsupported schema error");
458 };
459 assert_eq!(err.code(), crate::LeanLoaderDiagnosticCode::UnsupportedManifestSchema);
460 assert!(err.message().contains("unsupported Lean capability manifest schema"));
461 }
462
463 #[test]
464 fn built_capability_records_dependency_descriptors() {
465 let spec = LeanBuiltCapability::path("/tmp/libcap.so").dependency(
466 LeanLibraryDependency::path("/tmp/libdep.so")
467 .export_symbols_for_dependents()
468 .initializer("dep_pkg", "Dep"),
469 );
470
471 let dependencies = spec.dependency_descriptors();
472 assert_eq!(dependencies.len(), 1);
473 let Some(dependency) = dependencies.first() else {
474 panic!("expected one dependency descriptor");
475 };
476 assert!(dependency.exports_symbols_for_dependents());
477 let Some(initializer) = dependency.module_initializer() else {
478 panic!("dependency initializer is recorded");
479 };
480 assert_eq!(initializer.package_name(), "dep_pkg");
481 assert_eq!(initializer.module_name(), "Dep");
482 }
483
484 fn temp_manifest_path(name: &str) -> PathBuf {
485 let dir = std::env::temp_dir().join(format!("lean-rs-manifest-{}-{name}", std::process::id()));
486 drop(fs::remove_dir_all(&dir));
487 fs::create_dir_all(&dir).expect("create manifest test dir");
488 dir.join("capability.json")
489 }
490
491 fn write_manifest(path: &std::path::Path, contents: &str) {
492 fs::write(path, contents).expect("write manifest fixture");
493 }
494}