Skip to main content

lean_toolchain/
built_capability.rs

1//! Runtime descriptor for a Lean capability built by a downstream `build.rs`.
2//!
3//! The descriptor carries the identity of a Lean capability the consumer's
4//! build script embedded — dylib path or manifest path (either literal or via
5//! environment variable), Lake package and module names, and any dependent
6//! dylibs. It is pure data: it does not link `libleanshared`, so the worker
7//! parent crate can consume it without dragging the Lean runtime into its
8//! link graph.
9//!
10//! The runtime opener that turns a descriptor into a loaded capability lives
11//! in `lean-rs` (`LeanCapability`); the corresponding preflight runner that
12//! inspects exported symbols lives in `lean_rs::module::preflight`. Both keep
13//! the descriptor here as their input.
14//!
15//! Source-compat: `lean-rs` re-exports [`LeanBuiltCapability`] and
16//! [`LeanBuiltCapabilityError`] at their historical paths.
17
18use std::path::PathBuf;
19
20use crate::loader::LeanLibraryDependency;
21
22/// Runtime descriptor for a Lean capability built by a downstream `build.rs`.
23#[derive(Clone, Debug, Eq, PartialEq)]
24pub struct LeanBuiltCapability {
25    dylib_path: Option<PathBuf>,
26    env_var: Option<String>,
27    manifest_path: Option<PathBuf>,
28    manifest_env_var: Option<String>,
29    package: Option<String>,
30    module: Option<String>,
31    dependencies: Vec<LeanLibraryDependency>,
32}
33
34impl LeanBuiltCapability {
35    /// Build a descriptor from an embedded dylib path.
36    ///
37    /// This remains supported for simple or compatibility cases. Prefer
38    /// [`Self::manifest_path`] for shipped binaries because the manifest also
39    /// carries dependency and loader-order facts:
40    ///
41    /// ```ignore
42    /// let spec = lean_toolchain::LeanBuiltCapability::path(env!("LEAN_RS_CAPABILITY_MY_CAPABILITY_DYLIB"))
43    ///     .package("my_app")
44    ///     .module("MyCapability");
45    /// ```
46    #[must_use]
47    pub fn path(path: impl Into<PathBuf>) -> Self {
48        Self {
49            dylib_path: Some(path.into()),
50            env_var: None,
51            manifest_path: None,
52            manifest_env_var: None,
53            package: None,
54            module: None,
55            dependencies: Vec::new(),
56        }
57    }
58
59    /// Build a descriptor that resolves the dylib path from a runtime
60    /// environment variable.
61    ///
62    /// Prefer [`Self::path`] with Rust's `env!` macro for redistributable
63    /// binaries. Runtime environment lookup is useful for tests, local
64    /// overrides, and launcher-managed deployments.
65    #[must_use]
66    pub fn env(env_var: impl Into<String>) -> Self {
67        Self {
68            dylib_path: None,
69            env_var: Some(env_var.into()),
70            manifest_path: None,
71            manifest_env_var: None,
72            package: None,
73            module: None,
74            dependencies: Vec::new(),
75        }
76    }
77
78    /// Build a descriptor from an embedded artifact manifest path.
79    ///
80    /// This is the canonical form for shipped binaries using
81    /// `CargoLeanCapability`'s manifest output:
82    ///
83    /// ```ignore
84    /// let spec = lean_toolchain::LeanBuiltCapability::manifest_path(
85    ///     env!("LEAN_RS_CAPABILITY_MY_CAPABILITY_MANIFEST"),
86    /// );
87    /// ```
88    #[must_use]
89    pub fn manifest_path(path: impl Into<PathBuf>) -> Self {
90        Self {
91            dylib_path: None,
92            env_var: None,
93            manifest_path: Some(path.into()),
94            manifest_env_var: None,
95            package: None,
96            module: None,
97            dependencies: Vec::new(),
98        }
99    }
100
101    /// Build a descriptor that resolves the artifact manifest path from a
102    /// runtime environment variable.
103    ///
104    /// Prefer [`Self::manifest_path`] with Rust's `env!` macro for
105    /// redistributable binaries. Runtime environment lookup is useful for
106    /// tests, local overrides, and launcher-managed deployments.
107    #[must_use]
108    pub fn manifest_env(env_var: impl Into<String>) -> Self {
109        Self {
110            dylib_path: None,
111            env_var: None,
112            manifest_path: None,
113            manifest_env_var: Some(env_var.into()),
114            package: None,
115            module: None,
116            dependencies: Vec::new(),
117        }
118    }
119
120    /// Preserve the Cargo environment variable name for diagnostics.
121    #[must_use]
122    pub fn env_var(mut self, env_var: impl Into<String>) -> Self {
123        self.env_var = Some(env_var.into());
124        self
125    }
126
127    /// Preserve the Cargo manifest environment variable name for diagnostics.
128    #[must_use]
129    pub fn manifest_env_var(mut self, env_var: impl Into<String>) -> Self {
130        self.manifest_env_var = Some(env_var.into());
131        self
132    }
133
134    /// Set the Lake package name used by the Lean initializer.
135    #[must_use]
136    pub fn package(mut self, package: impl Into<String>) -> Self {
137        self.package = Some(package.into());
138        self
139    }
140
141    /// Set the root Lean module name initialized by Rust.
142    #[must_use]
143    pub fn module(mut self, module: impl Into<String>) -> Self {
144        self.module = Some(module.into());
145        self
146    }
147
148    /// Add a dependent Lean dylib that must stay alive with this capability.
149    #[must_use]
150    pub fn dependency(mut self, dependency: LeanLibraryDependency) -> Self {
151        self.dependencies.push(dependency);
152        self
153    }
154
155    /// Add multiple dependent Lean dylibs that must stay alive with this
156    /// capability.
157    #[must_use]
158    pub fn dependencies(mut self, dependencies: impl IntoIterator<Item = LeanLibraryDependency>) -> Self {
159        self.dependencies.extend(dependencies);
160        self
161    }
162
163    /// Return the configured package name.
164    #[must_use]
165    pub fn package_name(&self) -> Option<&str> {
166        self.package.as_deref()
167    }
168
169    /// Return the configured module name.
170    #[must_use]
171    pub fn module_name(&self) -> Option<&str> {
172        self.module.as_deref()
173    }
174
175    /// Take the configured package name, leaving `None` behind.
176    pub fn take_package_name(&mut self) -> Option<String> {
177        self.package.take()
178    }
179
180    /// Take the configured module name, leaving `None` behind.
181    pub fn take_module_name(&mut self) -> Option<String> {
182        self.module.take()
183    }
184
185    /// Take the recorded dependency descriptors, leaving an empty list.
186    pub fn take_dependencies(&mut self) -> Vec<LeanLibraryDependency> {
187        std::mem::take(&mut self.dependencies)
188    }
189
190    /// Dependency dylibs that will be opened before the primary capability.
191    #[must_use]
192    pub fn dependency_descriptors(&self) -> &[LeanLibraryDependency] {
193        &self.dependencies
194    }
195
196    /// Resolve the capability dylib path.
197    ///
198    /// # Errors
199    ///
200    /// Returns [`LeanBuiltCapabilityError::MissingDylibSource`] if no dylib
201    /// path or environment variable is configured, or
202    /// [`LeanBuiltCapabilityError::EnvVarNotSet`] if the configured environment
203    /// variable is not present at runtime.
204    pub fn dylib_path(&self) -> Result<PathBuf, LeanBuiltCapabilityError> {
205        if let Some(path) = &self.dylib_path {
206            return Ok(path.clone());
207        }
208        let env_var = self
209            .env_var
210            .as_deref()
211            .ok_or(LeanBuiltCapabilityError::MissingDylibSource)?;
212        std::env::var_os(env_var)
213            .map(PathBuf::from)
214            .ok_or_else(|| LeanBuiltCapabilityError::EnvVarNotSet {
215                env_var: env_var.to_owned(),
216                kind: BuiltCapabilityArtifact::Dylib,
217            })
218    }
219
220    /// Resolve the build artifact manifest path.
221    ///
222    /// # Errors
223    ///
224    /// Returns [`LeanBuiltCapabilityError::MissingManifestSource`] if no
225    /// manifest path or manifest environment variable is configured, or
226    /// [`LeanBuiltCapabilityError::EnvVarNotSet`] if the configured environment
227    /// variable is not present at runtime.
228    pub fn resolved_manifest_path(&self) -> Result<PathBuf, LeanBuiltCapabilityError> {
229        if let Some(path) = &self.manifest_path {
230            return Ok(path.clone());
231        }
232        let env_var = self
233            .manifest_env_var
234            .as_deref()
235            .ok_or(LeanBuiltCapabilityError::MissingManifestSource)?;
236        std::env::var_os(env_var)
237            .map(PathBuf::from)
238            .ok_or_else(|| LeanBuiltCapabilityError::EnvVarNotSet {
239                env_var: env_var.to_owned(),
240                kind: BuiltCapabilityArtifact::Manifest,
241            })
242    }
243}
244
245impl From<&crate::build_helpers::BuiltLeanCapability> for LeanBuiltCapability {
246    fn from(value: &crate::build_helpers::BuiltLeanCapability) -> Self {
247        Self {
248            dylib_path: Some(value.dylib_path().to_path_buf()),
249            env_var: Some(value.env_var().to_owned()),
250            manifest_path: Some(value.manifest_path().to_path_buf()),
251            manifest_env_var: Some(value.manifest_env_var().to_owned()),
252            package: Some(value.package().to_owned()),
253            module: Some(value.module().to_owned()),
254            dependencies: Vec::new(),
255        }
256    }
257}
258
259/// Which artifact a [`LeanBuiltCapability`] resolution was looking for.
260#[derive(Clone, Copy, Debug, Eq, PartialEq)]
261pub enum BuiltCapabilityArtifact {
262    /// The capability's primary dylib.
263    Dylib,
264    /// The capability's build artifact manifest.
265    Manifest,
266}
267
268impl BuiltCapabilityArtifact {
269    fn as_str(self) -> &'static str {
270        match self {
271            Self::Dylib => "Lean capability dylib",
272            Self::Manifest => "Lean capability manifest",
273        }
274    }
275}
276
277/// Errors returned when resolving a [`LeanBuiltCapability`] descriptor.
278#[derive(Clone, Debug, Eq, PartialEq)]
279pub enum LeanBuiltCapabilityError {
280    /// The descriptor has no dylib path and no dylib environment variable.
281    MissingDylibSource,
282    /// The descriptor has no manifest path and no manifest environment variable.
283    MissingManifestSource,
284    /// The configured environment variable is not set at runtime.
285    EnvVarNotSet {
286        /// Name of the environment variable that was queried.
287        env_var: String,
288        /// Which artifact the environment variable was supposed to point at.
289        kind: BuiltCapabilityArtifact,
290    },
291}
292
293impl std::fmt::Display for LeanBuiltCapabilityError {
294    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
295        match self {
296            Self::MissingDylibSource => {
297                f.write_str("LeanBuiltCapability needs either a dylib path or an environment variable")
298            }
299            Self::MissingManifestSource => {
300                f.write_str("LeanBuiltCapability needs either a manifest path or manifest environment variable")
301            }
302            Self::EnvVarNotSet { env_var, kind } => {
303                write!(f, "environment variable {env_var} is not set for {}", kind.as_str())
304            }
305        }
306    }
307}
308
309impl std::error::Error for LeanBuiltCapabilityError {}
310
311#[cfg(test)]
312#[allow(clippy::expect_used, clippy::panic)]
313mod tests {
314    use super::{BuiltCapabilityArtifact, LeanBuiltCapability, LeanBuiltCapabilityError};
315    use std::path::PathBuf;
316
317    #[test]
318    fn path_descriptor_resolves_without_runtime_env() {
319        let spec = LeanBuiltCapability::path("/tmp/libcap.so")
320            .env_var("LEAN_RS_CAPABILITY_CAP_DYLIB")
321            .package("pkg")
322            .module("Cap");
323
324        let path = match spec.dylib_path() {
325            Ok(path) => path,
326            Err(err) => panic!("expected path, got {err}"),
327        };
328        assert_eq!(path, PathBuf::from("/tmp/libcap.so"));
329        assert_eq!(spec.package_name(), Some("pkg"));
330        assert_eq!(spec.module_name(), Some("Cap"));
331    }
332
333    #[test]
334    fn missing_runtime_env_is_typed() {
335        let spec = LeanBuiltCapability::env("LEAN_TC_TEST_MISSING_CAPABILITY_DYLIB")
336            .package("pkg")
337            .module("Cap");
338        let err = match spec.dylib_path() {
339            Ok(path) => panic!("expected missing env error, got {}", path.display()),
340            Err(err) => err,
341        };
342        assert!(matches!(
343            err,
344            LeanBuiltCapabilityError::EnvVarNotSet {
345                kind: BuiltCapabilityArtifact::Dylib,
346                ..
347            }
348        ));
349    }
350
351    #[test]
352    fn missing_runtime_manifest_env_is_typed() {
353        let spec = LeanBuiltCapability::manifest_env("LEAN_TC_TEST_MISSING_CAPABILITY_MANIFEST");
354        let err = match spec.resolved_manifest_path() {
355            Ok(path) => panic!("expected missing env error, got {}", path.display()),
356            Err(err) => err,
357        };
358        assert!(matches!(
359            err,
360            LeanBuiltCapabilityError::EnvVarNotSet {
361                kind: BuiltCapabilityArtifact::Manifest,
362                ..
363            }
364        ));
365    }
366
367    #[test]
368    fn missing_dylib_source_is_typed() {
369        let spec = LeanBuiltCapability::manifest_path("/tmp/manifest.json");
370        let err = match spec.dylib_path() {
371            Ok(path) => panic!("expected missing dylib source error, got {}", path.display()),
372            Err(err) => err,
373        };
374        assert_eq!(err, LeanBuiltCapabilityError::MissingDylibSource);
375    }
376
377    #[test]
378    fn missing_manifest_source_is_typed() {
379        let spec = LeanBuiltCapability::path("/tmp/libcap.so").package("pkg").module("Cap");
380        let err = match spec.resolved_manifest_path() {
381            Ok(path) => panic!("expected missing manifest source error, got {}", path.display()),
382            Err(err) => err,
383        };
384        assert_eq!(err, LeanBuiltCapabilityError::MissingManifestSource);
385    }
386}