Skip to main content

lean_rs/module/
capability.rs

1//! Build-script paired capability opener.
2//!
3//! This module is the runtime half of
4//! [`lean_toolchain::CargoLeanCapability`]. It lets shipped crates open the
5//! dylib path their `build.rs` embedded without repeating environment-path
6//! lookup and module-initialization names at every call site.
7//!
8//! The [`LeanBuiltCapability`] descriptor itself is link-free and lives in
9//! `lean-toolchain` so the worker parent crate can consume it without
10//! relinking `libleanshared`. It is re-exported here for source compatibility.
11
12use std::path::Path;
13
14use super::preflight::{CapabilityManifest, LeanRuntimePreflight, manifest_error_to_lean_error, report_into_error};
15use super::{LeanLibrary, LeanLibraryBundle, LeanLibraryDependency, LeanModule};
16use crate::error::{LeanError, LeanResult};
17use crate::runtime::LeanRuntime;
18
19// Build-script descriptor lives in `lean-toolchain` (below `lean-rs`) so the
20// worker parent crate can construct and consume it without relinking
21// `libleanshared`. Re-exported here for the historical `lean_rs::LeanBuiltCapability`
22// path.
23pub use lean_toolchain::{BuiltCapabilityArtifact, LeanBuiltCapability, LeanBuiltCapabilityError};
24
25fn built_capability_error_to_lean_error(err: &LeanBuiltCapabilityError) -> LeanError {
26    LeanError::module_init(err.to_string())
27}
28
29/// Opened Lean capability whose dylib path and initializer names came from
30/// the build-script pairing.
31pub struct LeanCapability<'lean> {
32    bundle: LeanLibraryBundle<'lean>,
33    package: String,
34    module: String,
35}
36
37impl<'lean> LeanCapability<'lean> {
38    /// Open and initialize a build-script produced Lean capability from its
39    /// JSON artifact manifest.
40    ///
41    /// # Errors
42    ///
43    /// Returns [`LeanError`] when the manifest path cannot be resolved, the
44    /// manifest is missing, malformed, or unsupported, or the bundle described
45    /// by the manifest cannot be opened.
46    #[allow(clippy::needless_pass_by_value)]
47    pub fn from_build_manifest(runtime: &'lean LeanRuntime, spec: LeanBuiltCapability) -> LeanResult<Self> {
48        let report = LeanRuntimePreflight::new(spec.clone()).check();
49        if !report.is_ok() {
50            return Err(report_into_error(report));
51        }
52        let manifest_path = spec
53            .resolved_manifest_path()
54            .map_err(|err| built_capability_error_to_lean_error(&err))?;
55        let manifest = CapabilityManifest::read(&manifest_path).map_err(manifest_error_to_lean_error)?;
56        Self::open_with_dependencies(
57            runtime,
58            manifest.primary_dylib,
59            manifest.package,
60            manifest.module,
61            manifest.dependencies,
62        )
63    }
64
65    /// Open and initialize a build-script produced Lean capability from a
66    /// direct dylib path.
67    ///
68    /// This compatibility path cannot carry dependency ordering by itself.
69    /// Prefer [`Self::from_build_manifest`] for shipped crates.
70    ///
71    /// # Errors
72    ///
73    /// Returns [`LeanError`] when the dylib path cannot be resolved, the
74    /// dynamic loader cannot open it, or the configured module initializer
75    /// fails.
76    pub fn from_build_env(runtime: &'lean LeanRuntime, mut spec: LeanBuiltCapability) -> LeanResult<Self> {
77        let dylib_path = spec
78            .dylib_path()
79            .map_err(|err| built_capability_error_to_lean_error(&err))?;
80        let package = spec.take_package_name().ok_or_else(|| {
81            LeanError::linking("LeanBuiltCapability is missing the Lake package name; call `.package(...)`")
82        })?;
83        let module = spec.take_module_name().ok_or_else(|| {
84            LeanError::linking("LeanBuiltCapability is missing the root Lean module name; call `.module(...)`")
85        })?;
86        let dependencies = spec.take_dependencies();
87        Self::open_with_dependencies(runtime, dylib_path, package, module, dependencies)
88    }
89
90    /// Open and initialize a capability from an explicit dylib path and
91    /// initializer names.
92    ///
93    /// # Errors
94    ///
95    /// Returns [`LeanError`] when the dynamic loader cannot open the dylib or
96    /// the configured module initializer fails.
97    pub fn open(
98        runtime: &'lean LeanRuntime,
99        dylib_path: impl AsRef<Path>,
100        package: impl Into<String>,
101        module: impl Into<String>,
102    ) -> LeanResult<Self> {
103        let package = package.into();
104        let module = module.into();
105        Self::open_with_dependencies(runtime, dylib_path, package, module, [])
106    }
107
108    /// Open and initialize a capability with explicitly described dependency
109    /// dylibs.
110    ///
111    /// This is the runtime form artifact manifests feed. Use
112    /// [`LeanCapability::from_build_manifest`] for shipped crates when
113    /// build-script metadata is available.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`LeanError`] when a dependency or primary dylib cannot be
118    /// loaded, or when a dependency or primary module initializer fails.
119    pub fn open_with_dependencies(
120        runtime: &'lean LeanRuntime,
121        dylib_path: impl AsRef<Path>,
122        package: impl Into<String>,
123        module: impl Into<String>,
124        dependencies: impl IntoIterator<Item = LeanLibraryDependency>,
125    ) -> LeanResult<Self> {
126        let package = package.into();
127        let module = module.into();
128        let bundle = LeanLibraryBundle::open(runtime, dylib_path, dependencies)?;
129        let _module = bundle.initialize_module(&package, &module)?;
130        Ok(Self {
131            bundle,
132            package,
133            module,
134        })
135    }
136
137    /// Return an initialized module handle.
138    ///
139    /// Lean module initializers are idempotent, so obtaining the handle after
140    /// construction is cheap and safe.
141    ///
142    /// # Errors
143    ///
144    /// Returns [`LeanError`] if the module initializer unexpectedly fails when
145    /// invoked again.
146    pub fn module(&self) -> LeanResult<LeanModule<'lean, '_>> {
147        self.bundle.initialize_module(&self.package, &self.module)
148    }
149
150    /// Borrow the underlying library for advanced symbol access.
151    #[must_use]
152    pub fn library(&self) -> &LeanLibrary<'lean> {
153        self.bundle.library()
154    }
155
156    /// Borrow the bundle that anchors this capability and its dependencies.
157    #[must_use]
158    pub fn bundle(&self) -> &LeanLibraryBundle<'lean> {
159        &self.bundle
160    }
161
162    /// Lake package name used by the initializer.
163    #[must_use]
164    pub fn package_name(&self) -> &str {
165        &self.package
166    }
167
168    /// Root Lean module name used by the initializer.
169    #[must_use]
170    pub fn module_name(&self) -> &str {
171        &self.module
172    }
173}
174
175#[cfg(test)]
176#[allow(clippy::expect_used, clippy::panic)]
177mod tests {
178    use super::{
179        BuiltCapabilityArtifact, CapabilityManifest, LeanBuiltCapability, LeanBuiltCapabilityError,
180        LeanLibraryDependency,
181    };
182    use std::fs;
183    use std::path::PathBuf;
184
185    #[test]
186    fn built_capability_path_is_resolved_without_runtime_env() {
187        let spec = LeanBuiltCapability::path("/tmp/libcap.so")
188            .env_var("LEAN_RS_CAPABILITY_CAP_DYLIB")
189            .package("pkg")
190            .module("Cap");
191
192        let path = match spec.dylib_path() {
193            Ok(path) => path,
194            Err(err) => panic!("expected path, got {err}"),
195        };
196        assert_eq!(path, std::path::PathBuf::from("/tmp/libcap.so"));
197        assert_eq!(spec.package_name(), Some("pkg"));
198        assert_eq!(spec.module_name(), Some("Cap"));
199    }
200
201    #[test]
202    fn missing_runtime_env_is_typed() {
203        let spec = LeanBuiltCapability::env("LEAN_RS_TEST_MISSING_CAPABILITY_DYLIB")
204            .package("pkg")
205            .module("Cap");
206        let err = match spec.dylib_path() {
207            Ok(path) => panic!("expected missing env error, got {}", path.display()),
208            Err(err) => err,
209        };
210        assert!(matches!(
211            err,
212            LeanBuiltCapabilityError::EnvVarNotSet {
213                kind: BuiltCapabilityArtifact::Dylib,
214                ..
215            }
216        ));
217    }
218
219    #[test]
220    fn missing_runtime_manifest_env_is_typed() {
221        let spec = LeanBuiltCapability::manifest_env("LEAN_RS_TEST_MISSING_CAPABILITY_MANIFEST");
222        let err = match spec.resolved_manifest_path() {
223            Ok(path) => panic!("expected missing manifest env error, got {}", path.display()),
224            Err(err) => err,
225        };
226        assert!(matches!(
227            err,
228            LeanBuiltCapabilityError::EnvVarNotSet {
229                kind: BuiltCapabilityArtifact::Manifest,
230                ..
231            }
232        ));
233    }
234
235    #[test]
236    fn manifest_descriptor_parses_dependencies() {
237        let path = temp_manifest_path("manifest_descriptor_parses_dependencies");
238        write_manifest(
239            &path,
240            r#"{
241  "schema_version": 1,
242  "target_name": "Cap",
243  "package": "pkg",
244  "module": "Cap",
245  "primary_dylib": "/tmp/libcap.so",
246  "dependencies": [
247    {
248      "dylib_path": "/tmp/libdep.so",
249      "export_symbols_for_dependents": true,
250      "initializer": { "package": "dep_pkg", "module": "Dep" }
251    }
252  ]
253}"#,
254        );
255
256        let manifest = match CapabilityManifest::read(&path) {
257            Ok(manifest) => manifest,
258            Err(err) => panic!("expected manifest to parse, got {err}"),
259        };
260        assert_eq!(manifest.primary_dylib, PathBuf::from("/tmp/libcap.so"));
261        assert_eq!(manifest.package, "pkg");
262        assert_eq!(manifest.module, "Cap");
263        assert_eq!(manifest.dependencies.len(), 1);
264        let Some(dependency) = manifest.dependencies.first() else {
265            panic!("expected one dependency");
266        };
267        assert!(dependency.exports_symbols_for_dependents());
268        assert_eq!(dependency.path_ref(), std::path::Path::new("/tmp/libdep.so"));
269        let Some(initializer) = dependency.module_initializer() else {
270            panic!("expected dependency initializer");
271        };
272        assert_eq!(initializer.package_name(), "dep_pkg");
273        assert_eq!(initializer.module_name(), "Dep");
274    }
275
276    #[test]
277    fn unsupported_manifest_schema_is_typed() {
278        let path = temp_manifest_path("unsupported_manifest_schema_is_typed");
279        write_manifest(
280            &path,
281            r#"{
282  "schema_version": 999,
283  "package": "pkg",
284  "module": "Cap",
285  "primary_dylib": "/tmp/libcap.so"
286}"#,
287        );
288
289        let Err(err) = CapabilityManifest::read(&path) else {
290            panic!("expected unsupported schema error");
291        };
292        assert_eq!(err.code(), crate::LeanLoaderDiagnosticCode::UnsupportedManifestSchema);
293        assert!(err.message().contains("unsupported Lean capability manifest schema"));
294    }
295
296    #[test]
297    fn built_capability_records_dependency_descriptors() {
298        let spec = LeanBuiltCapability::path("/tmp/libcap.so").dependency(
299            LeanLibraryDependency::path("/tmp/libdep.so")
300                .export_symbols_for_dependents()
301                .initializer("dep_pkg", "Dep"),
302        );
303
304        let dependencies = spec.dependency_descriptors();
305        assert_eq!(dependencies.len(), 1);
306        let Some(dependency) = dependencies.first() else {
307            panic!("expected one dependency descriptor");
308        };
309        assert!(dependency.exports_symbols_for_dependents());
310        let Some(initializer) = dependency.module_initializer() else {
311            panic!("dependency initializer is recorded");
312        };
313        assert_eq!(initializer.package_name(), "dep_pkg");
314        assert_eq!(initializer.module_name(), "Dep");
315    }
316
317    fn temp_manifest_path(name: &str) -> PathBuf {
318        let dir = std::env::temp_dir().join(format!("lean-rs-manifest-{}-{name}", std::process::id()));
319        drop(fs::remove_dir_all(&dir));
320        fs::create_dir_all(&dir).expect("create manifest test dir");
321        dir.join("capability.json")
322    }
323
324    fn write_manifest(path: &std::path::Path, contents: &str) {
325        fs::write(path, contents).expect("write manifest fixture");
326    }
327}