lean_rs/module/bundle.rs
1//! Capability-level loader that anchors dependent Lean libraries.
2//!
3//! [`LeanLibrary`] is intentionally low-level: it owns one dynamic-loader
4//! handle. [`LeanLibraryBundle`] is the shipped-capability boundary. It opens
5//! dependency dylibs first, gives them exported symbol visibility when a later
6//! Lean dylib needs it, initializes dependency modules when requested, and keeps
7//! every handle alive until the primary capability is dropped.
8
9use std::path::{Path, PathBuf};
10
11use super::{LeanLibrary, LeanModule};
12use crate::error::LeanResult;
13use crate::runtime::LeanRuntime;
14
15/// Initializer for a Lean module hosted by a loaded dylib.
16#[derive(Clone, Debug, Eq, PartialEq)]
17pub struct LeanModuleInitializer {
18 package: String,
19 module: String,
20}
21
22impl LeanModuleInitializer {
23 /// Create an initializer descriptor from Lake package and root module names.
24 #[must_use]
25 pub fn new(package: impl Into<String>, module: impl Into<String>) -> Self {
26 Self {
27 package: package.into(),
28 module: module.into(),
29 }
30 }
31
32 /// Lake package name used by the initializer.
33 #[must_use]
34 pub fn package_name(&self) -> &str {
35 &self.package
36 }
37
38 /// Root Lean module name used by the initializer.
39 #[must_use]
40 pub fn module_name(&self) -> &str {
41 &self.module
42 }
43}
44
45/// Dependency dylib that must stay alive while a capability is loaded.
46#[derive(Clone, Debug, Eq, PartialEq)]
47pub struct LeanLibraryDependency {
48 path: PathBuf,
49 exports_symbols_for_dependents: bool,
50 initializer: Option<LeanModuleInitializer>,
51}
52
53impl LeanLibraryDependency {
54 /// Add a dependency dylib to the bundle.
55 #[must_use]
56 pub fn path(path: impl Into<PathBuf>) -> Self {
57 Self {
58 path: path.into(),
59 exports_symbols_for_dependents: false,
60 initializer: None,
61 }
62 }
63
64 /// Make this dependency's Lean symbols available to later dylibs in the
65 /// same bundle.
66 ///
67 /// This is a capability-level requirement, not a platform-loader flag in
68 /// the public contract. On ELF platforms it maps to global symbol
69 /// visibility; other platforms use the equivalent behavior provided by the
70 /// native loader.
71 #[must_use]
72 pub fn export_symbols_for_dependents(mut self) -> Self {
73 self.exports_symbols_for_dependents = true;
74 self
75 }
76
77 /// Initialize a module from this dependency after it is opened.
78 #[must_use]
79 pub fn initializer(mut self, package: impl Into<String>, module: impl Into<String>) -> Self {
80 self.initializer = Some(LeanModuleInitializer::new(package, module));
81 self
82 }
83
84 /// On-disk path to the dependency dylib.
85 #[must_use]
86 pub fn path_ref(&self) -> &Path {
87 &self.path
88 }
89
90 /// Whether symbols from this dependency are exported to later bundle
91 /// members.
92 #[must_use]
93 pub fn exports_symbols_for_dependents(&self) -> bool {
94 self.exports_symbols_for_dependents
95 }
96
97 /// Optional module initializer for this dependency.
98 #[must_use]
99 pub fn module_initializer(&self) -> Option<&LeanModuleInitializer> {
100 self.initializer.as_ref()
101 }
102}
103
104/// A primary Lean capability plus its dependent Lean dylibs.
105///
106/// The primary library is dropped before dependency libraries so imported
107/// symbols remain available for as long as capability code can run.
108pub struct LeanLibraryBundle<'lean> {
109 primary: LeanLibrary<'lean>,
110 dependencies: Vec<LeanLibrary<'lean>>,
111}
112
113impl<'lean> LeanLibraryBundle<'lean> {
114 /// Open a primary capability dylib and every dependency it needs.
115 ///
116 /// Dependencies are opened and optionally initialized in iterator order.
117 /// The primary dylib is opened last. Every dependency handle is stored
118 /// inside the bundle, so helper functions can return a bundle without
119 /// leaking handles or relying on platform loader state that outlives Rust's
120 /// RAII values.
121 ///
122 /// # Errors
123 ///
124 /// Returns [`crate::LeanError`] if any dependency or primary dylib cannot
125 /// be opened, or if any requested dependency initializer fails.
126 pub fn open(
127 runtime: &'lean LeanRuntime,
128 primary_path: impl AsRef<Path>,
129 dependencies: impl IntoIterator<Item = LeanLibraryDependency>,
130 ) -> LeanResult<Self> {
131 let mut opened_dependencies = Vec::new();
132 for dependency in dependencies {
133 let library = if dependency.exports_symbols_for_dependents {
134 LeanLibrary::open_globally(runtime, &dependency.path)?
135 } else {
136 LeanLibrary::open(runtime, &dependency.path)?
137 };
138 if let Some(initializer) = dependency.initializer {
139 let _module = library.initialize_module(&initializer.package, &initializer.module)?;
140 }
141 opened_dependencies.push(library);
142 }
143
144 let primary = LeanLibrary::open(runtime, primary_path)?;
145 Ok(Self {
146 primary,
147 dependencies: opened_dependencies,
148 })
149 }
150
151 /// Initialize a module from the primary capability library.
152 ///
153 /// This delegates to [`LeanLibrary::initialize_module`] while preserving the
154 /// bundle lifetime that keeps dependent dylibs anchored.
155 ///
156 /// # Errors
157 ///
158 /// Returns [`crate::LeanError`] if the requested module name cannot be
159 /// mapped to an initializer symbol, if that symbol is missing from the
160 /// primary library, or if the Lean initializer returns `IO.error`.
161 pub fn initialize_module<'bundle>(
162 &'bundle self,
163 package: &str,
164 module: &str,
165 ) -> LeanResult<LeanModule<'lean, 'bundle>> {
166 self.primary.initialize_module(package, module)
167 }
168
169 /// Borrow the primary capability library.
170 #[must_use]
171 pub fn library(&self) -> &LeanLibrary<'lean> {
172 &self.primary
173 }
174
175 /// Number of dependency dylibs anchored by this bundle.
176 #[must_use]
177 pub fn dependency_count(&self) -> usize {
178 self.dependencies.len()
179 }
180}
181
182impl std::fmt::Debug for LeanLibraryBundle<'_> {
183 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
184 f.debug_struct("LeanLibraryBundle")
185 .field("primary", &self.primary)
186 .field("dependency_count", &self.dependencies.len())
187 .finish()
188 }
189}