#![forbid(unsafe_code)]
#![deny(missing_docs)]
mod acceptance;
mod args;
mod envelope;
mod evidence;
mod render;
use std::sync::Arc;
use sim_kernel::{
AbiVersion, Args, Callable, CapabilityName, Cx, Error, Export, Lib, LibManifest, LibTarget,
Linker, LoadCx, Object, ObjectCompat, Result, Symbol, Value, Version,
};
pub use args::{
AcceptanceAction, AcceptanceRequest, ComputeCommand, OutputMode, ProfileAction,
parse_compute_args,
};
pub use render::help;
use crate::{
acceptance::acceptance_evidence,
envelope::envelope_args,
evidence::{profile_evidence, provider_rows, recipe_evidence},
render::{render_acceptance, render_profile, render_providers, render_recipe},
};
pub fn compute_device_capability() -> CapabilityName {
CapabilityName::new("compute.device")
}
pub fn compute_profile_read_capability() -> CapabilityName {
CapabilityName::new("compute.profile.read")
}
pub fn compute_profile_write_capability() -> CapabilityName {
CapabilityName::new("compute.profile.write")
}
pub fn compute_acceptance_capability() -> CapabilityName {
CapabilityName::new("compute.acceptance")
}
pub fn compute_cli_lib_symbol() -> Symbol {
Symbol::qualified("lib", "compute-cli")
}
pub fn compute_entrypoint_symbol() -> Symbol {
Symbol::qualified("cli", "main/compute")
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ComputeCliError {
message: String,
}
impl ComputeCliError {
pub fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
fn from_kernel(error: Error) -> Self {
Self::new(error.to_string())
}
}
impl std::fmt::Display for ComputeCliError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter.write_str(&self.message)
}
}
impl std::error::Error for ComputeCliError {}
#[derive(Clone, Default)]
pub struct ComputeCliLib {
profile_store: Option<Value>,
}
impl ComputeCliLib {
pub fn new() -> Self {
Self::default()
}
pub fn with_profile_store(profile_store: Value) -> Self {
Self {
profile_store: Some(profile_store),
}
}
}
impl Lib for ComputeCliLib {
fn manifest(&self) -> LibManifest {
LibManifest {
id: compute_cli_lib_symbol(),
version: Version(env!("CARGO_PKG_VERSION").to_owned()),
abi: AbiVersion { major: 0, minor: 1 },
target: LibTarget::HostRegistered,
requires: Vec::new(),
capabilities: Vec::new(),
exports: vec![Export::Function {
symbol: compute_entrypoint_symbol(),
function_id: None,
}],
}
}
fn load(&self, cx: &mut LoadCx, linker: &mut Linker<'_>) -> Result<()> {
linker.function_value(
compute_entrypoint_symbol(),
cx.factory().opaque(Arc::new(ComputeEntrypoint {
profile_store: self.profile_store.clone(),
}))?,
)?;
Ok(())
}
}
#[derive(Clone)]
struct ComputeEntrypoint {
profile_store: Option<Value>,
}
impl Object for ComputeEntrypoint {
fn display(&self, _cx: &mut Cx) -> Result<String> {
Ok("cli/main/compute".to_owned())
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
}
impl ObjectCompat for ComputeEntrypoint {
fn as_callable(&self) -> Option<&dyn Callable> {
Some(self)
}
}
impl Callable for ComputeEntrypoint {
fn call(&self, cx: &mut Cx, args: Args) -> Result<Value> {
let Some(envelope) = args.values().first() else {
return Err(Error::Eval("missing compute envelope".to_owned()));
};
let args = envelope_args(cx, envelope)?;
let command = parse_compute_args(&args).map_err(|err| Error::Eval(err.to_string()))?;
let output = run_command(cx, self.profile_store.as_ref(), &command)
.map_err(|err| Error::Eval(err.to_string()))?;
print!("{output}");
cx.factory().bool(true)
}
}
pub fn run_command(
cx: &mut Cx,
profile_store: Option<&Value>,
command: &ComputeCommand,
) -> std::result::Result<String, ComputeCliError> {
match command {
ComputeCommand::Help => Ok(help().to_owned()),
ComputeCommand::Devices(selection) | ComputeCommand::Probe(selection) => {
cx.require(&compute_device_capability())
.map_err(ComputeCliError::from_kernel)?;
Ok(render_providers(command, &provider_rows(cx, selection)))
}
ComputeCommand::Profile(request) => {
let capability = match request.action {
ProfileAction::Save => compute_profile_write_capability(),
ProfileAction::List | ProfileAction::Read => compute_profile_read_capability(),
};
cx.require(&capability)
.map_err(ComputeCliError::from_kernel)?;
let evidence = profile_evidence(cx, profile_store, request)?;
Ok(render_profile(command, &evidence))
}
ComputeCommand::Explain(request) => {
cx.require(&compute_device_capability())
.map_err(ComputeCliError::from_kernel)?;
cx.require(&compute_profile_read_capability())
.map_err(ComputeCliError::from_kernel)?;
let evidence = profile_evidence(cx, profile_store, request)?;
Ok(render_profile(command, &evidence))
}
ComputeCommand::Recipe(_) => {
cx.require(&compute_device_capability())
.map_err(ComputeCliError::from_kernel)?;
Ok(render_recipe(command, &recipe_evidence()))
}
ComputeCommand::Acceptance(request) => {
if matches!(request.action, AcceptanceAction::Capture) {
cx.require(&compute_acceptance_capability())
.map_err(ComputeCliError::from_kernel)?;
}
Ok(render_acceptance(&acceptance_evidence(request)?))
}
}
}
pub static RECIPES: sim_cookbook::EmbeddedDir =
include!(concat!(env!("OUT_DIR"), "/cookbook_recipes.rs"));
#[cfg(test)]
mod tests;