Skip to main content

sim_lib_compute_cli/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3//! Loadable compute command surface.
4//!
5//! The crate exports `cli/main/compute` as a host-registered callable. It
6//! inspects installed compute sites, profile storage supplied as a Table/Dir
7//! value by the embedding host, and physical acceptance artifacts; it creates no
8//! separate bootstrap.
9
10mod acceptance;
11mod args;
12mod envelope;
13mod evidence;
14mod render;
15
16use std::sync::Arc;
17
18use sim_kernel::{
19    AbiVersion, Args, Callable, CapabilityName, Cx, Error, Export, Lib, LibManifest, LibTarget,
20    Linker, LoadCx, Object, ObjectCompat, Result, Symbol, Value, Version,
21};
22
23pub use args::{
24    AcceptanceAction, AcceptanceRequest, ComputeCommand, OutputMode, ProfileAction,
25    parse_compute_args,
26};
27pub use render::help;
28
29use crate::{
30    acceptance::acceptance_evidence,
31    envelope::envelope_args,
32    evidence::{profile_evidence, provider_rows, recipe_evidence},
33    render::{render_acceptance, render_profile, render_providers, render_recipe},
34};
35
36/// Capability required for device inspection and probe evidence.
37pub fn compute_device_capability() -> CapabilityName {
38    CapabilityName::new("compute.device")
39}
40
41/// Capability required for profile reads.
42pub fn compute_profile_read_capability() -> CapabilityName {
43    CapabilityName::new("compute.profile.read")
44}
45
46/// Capability required for profile writes.
47pub fn compute_profile_write_capability() -> CapabilityName {
48    CapabilityName::new("compute.profile.write")
49}
50
51/// Capability required for physical acceptance capture.
52pub fn compute_acceptance_capability() -> CapabilityName {
53    CapabilityName::new("compute.acceptance")
54}
55
56/// Runtime library symbol for the compute CLI.
57pub fn compute_cli_lib_symbol() -> Symbol {
58    Symbol::qualified("lib", "compute-cli")
59}
60
61/// Symbol exported by the compute command entrypoint.
62pub fn compute_entrypoint_symbol() -> Symbol {
63    Symbol::qualified("cli", "main/compute")
64}
65
66/// Error returned by compute command parsing and rendering.
67#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct ComputeCliError {
69    message: String,
70}
71
72impl ComputeCliError {
73    /// Builds an error from a user-facing message.
74    pub fn new(message: impl Into<String>) -> Self {
75        Self {
76            message: message.into(),
77        }
78    }
79
80    fn from_kernel(error: Error) -> Self {
81        Self::new(error.to_string())
82    }
83}
84
85impl std::fmt::Display for ComputeCliError {
86    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87        formatter.write_str(&self.message)
88    }
89}
90
91impl std::error::Error for ComputeCliError {}
92
93/// Host-registered library that exports the compute command entrypoint.
94#[derive(Clone, Default)]
95pub struct ComputeCliLib {
96    profile_store: Option<Value>,
97}
98
99impl ComputeCliLib {
100    /// Builds the library without profile storage.
101    pub fn new() -> Self {
102        Self::default()
103    }
104
105    /// Builds the library with caller-supplied Table/Dir profile storage.
106    pub fn with_profile_store(profile_store: Value) -> Self {
107        Self {
108            profile_store: Some(profile_store),
109        }
110    }
111}
112
113impl Lib for ComputeCliLib {
114    fn manifest(&self) -> LibManifest {
115        LibManifest {
116            id: compute_cli_lib_symbol(),
117            version: Version(env!("CARGO_PKG_VERSION").to_owned()),
118            abi: AbiVersion { major: 0, minor: 1 },
119            target: LibTarget::HostRegistered,
120            requires: Vec::new(),
121            capabilities: Vec::new(),
122            exports: vec![Export::Function {
123                symbol: compute_entrypoint_symbol(),
124                function_id: None,
125            }],
126        }
127    }
128
129    fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
130        linker.function_value(
131            compute_entrypoint_symbol(),
132            cx.factory().opaque(Arc::new(ComputeEntrypoint {
133                profile_store: self.profile_store.clone(),
134            }))?,
135        )?;
136        Ok(())
137    }
138}
139
140#[derive(Clone)]
141struct ComputeEntrypoint {
142    profile_store: Option<Value>,
143}
144
145impl Object for ComputeEntrypoint {
146    fn display(&self, _cx: &mut Cx) -> Result<String> {
147        Ok("cli/main/compute".to_owned())
148    }
149
150    fn as_any(&self) -> &dyn std::any::Any {
151        self
152    }
153}
154
155impl ObjectCompat for ComputeEntrypoint {
156    fn as_callable(&self) -> Option<&dyn Callable> {
157        Some(self)
158    }
159}
160
161impl Callable for ComputeEntrypoint {
162    fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
163        let Some(envelope) = args.values().first() else {
164            return Err(Error::Eval("missing compute envelope".to_owned()));
165        };
166        let args = envelope_args(cx, envelope)?;
167        let command = parse_compute_args(&args).map_err(|err| Error::Eval(err.to_string()))?;
168        let output = run_command(cx, self.profile_store.as_ref(), &command)
169            .map_err(|err| Error::Eval(err.to_string()))?;
170        print!("{output}");
171        cx.factory().bool(true)
172    }
173}
174
175/// Runs a parsed compute command and returns rendered output.
176pub fn run_command(
177    cx: &mut Cx,
178    profile_store: Option<&Value>,
179    command: &ComputeCommand,
180) -> std::result::Result<String, ComputeCliError> {
181    match command {
182        ComputeCommand::Help => Ok(help().to_owned()),
183        ComputeCommand::Devices(selection) | ComputeCommand::Probe(selection) => {
184            cx.require(&compute_device_capability())
185                .map_err(ComputeCliError::from_kernel)?;
186            Ok(render_providers(command, &provider_rows(cx, selection)))
187        }
188        ComputeCommand::Profile(request) => {
189            let capability = match request.action {
190                ProfileAction::Save => compute_profile_write_capability(),
191                ProfileAction::List | ProfileAction::Read => compute_profile_read_capability(),
192            };
193            cx.require(&capability)
194                .map_err(ComputeCliError::from_kernel)?;
195            let evidence = profile_evidence(cx, profile_store, request)?;
196            Ok(render_profile(command, &evidence))
197        }
198        ComputeCommand::Explain(request) => {
199            cx.require(&compute_device_capability())
200                .map_err(ComputeCliError::from_kernel)?;
201            cx.require(&compute_profile_read_capability())
202                .map_err(ComputeCliError::from_kernel)?;
203            let evidence = profile_evidence(cx, profile_store, request)?;
204            Ok(render_profile(command, &evidence))
205        }
206        ComputeCommand::Recipe(_) => {
207            cx.require(&compute_device_capability())
208                .map_err(ComputeCliError::from_kernel)?;
209            Ok(render_recipe(command, &recipe_evidence()))
210        }
211        ComputeCommand::Acceptance(request) => {
212            if matches!(request.action, AcceptanceAction::Capture) {
213                cx.require(&compute_acceptance_capability())
214                    .map_err(ComputeCliError::from_kernel)?;
215            }
216            Ok(render_acceptance(&acceptance_evidence(request)?))
217        }
218    }
219}
220
221/// Cookbook recipes for this lib, embedded at build time.
222pub static RECIPES: sim_cookbook::EmbeddedDir =
223    include!(concat!(env!("OUT_DIR"), "/cookbook_recipes.rs"));
224
225#[cfg(test)]
226mod tests;