1use sim_kernel::{Args, Cx, ExportKind, ExportRecord, ExportState, Symbol};
2
3use crate::{
4 CliBoot, CliEnvelope, CliError, LoadReceipt, LoadReceiptRole, LoadSession,
5 envelope::cli_envelope_value, exit::value_to_exit_code,
6};
7
8pub const CLI_MAIN_ENTRYPOINT: &str = "cli/main";
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub struct CliEntrypoint {
14 pub lib: Symbol,
16 pub symbol: Symbol,
18}
19
20impl LoadSession {
21 pub fn run_loaded_boot(&mut self, boot: &CliBoot) -> Result<i32, CliError> {
23 if boot.config_report.is_some() {
24 print!("{}", self.run_config_report(boot)?);
25 return Ok(0);
26 }
27 if boot.list || boot.inspect.is_some() {
28 print!("{}", self.run_loaded_introspection(boot)?);
29 return Ok(0);
30 }
31 self.load_boot(boot)?;
32 self.run_loaded_handoff(&boot.envelope())
33 }
34
35 pub fn run_loaded_handoff(&mut self, envelope: &CliEnvelope) -> Result<i32, CliError> {
37 let entrypoint = select_cli_entrypoint(self.receipts(), envelope.verb.as_deref())?;
38 run_loaded_cli(self.cx_mut(), &entrypoint, envelope)
39 }
40}
41
42pub fn cli_main_entrypoint_symbol(name: &str) -> Symbol {
44 Symbol::qualified("cli", format!("main/{name}"))
45}
46
47pub fn select_cli_entrypoint(
52 receipts: &[LoadReceipt],
53 verb: Option<&str>,
54) -> Result<CliEntrypoint, CliError> {
55 select_from_role(
56 receipts,
57 |role| matches!(role, LoadReceiptRole::Library),
58 verb,
59 )
60 .or_else(|| {
61 select_from_role(
62 receipts,
63 |role| matches!(role, LoadReceiptRole::BootCodec { .. }),
64 verb,
65 )
66 })
67 .ok_or_else(|| no_entrypoint_error(receipts, verb))
68}
69
70pub fn run_loaded_cli(
72 cx: &mut Cx,
73 entrypoint: &CliEntrypoint,
74 envelope: &CliEnvelope,
75) -> Result<i32, CliError> {
76 let envelope = cli_envelope_value(cx, envelope)?;
77 let result = cx
78 .call_function(&entrypoint.symbol, Args::new(vec![envelope]))
79 .map_err(|err| {
80 CliError::new(format!(
81 "cli handoff failed for {} from {}: {err}",
82 entrypoint.symbol, entrypoint.lib
83 ))
84 })?;
85 value_to_exit_code(cx, result)
86}
87
88fn select_from_role(
89 receipts: &[LoadReceipt],
90 role_matches: impl Fn(&LoadReceiptRole) -> bool + Copy,
91 verb: Option<&str>,
92) -> Option<CliEntrypoint> {
93 if let Some(verb) = verb {
94 find_entrypoint(receipts, role_matches, |record| {
95 record_claims_exact_cli_main(record, verb)
96 })
97 .or_else(|| find_entrypoint(receipts, role_matches, record_claims_generic_cli_main))
98 } else {
99 find_entrypoint(receipts, role_matches, record_claims_cli_main)
100 }
101}
102
103fn find_entrypoint(
104 receipts: &[LoadReceipt],
105 role_matches: impl Fn(&LoadReceiptRole) -> bool,
106 record_matches: impl Fn(&ExportRecord) -> bool + Copy,
107) -> Option<CliEntrypoint> {
108 receipts
109 .iter()
110 .filter(|receipt| role_matches(&receipt.role))
111 .find_map(|receipt| entrypoint_for_receipt(receipt, record_matches))
112}
113
114fn entrypoint_for_receipt(
115 receipt: &LoadReceipt,
116 record_matches: impl Fn(&ExportRecord) -> bool + Copy,
117) -> Option<CliEntrypoint> {
118 receipt
119 .exports
120 .iter()
121 .find(|record| record_matches(record))
122 .map(|record| CliEntrypoint {
123 lib: receipt.manifest.id.clone(),
124 symbol: record.symbol.clone(),
125 })
126}
127
128fn record_claims_exact_cli_main(record: &ExportRecord, verb: &str) -> bool {
129 record.kind == ExportKind::named(ExportKind::FUNCTION)
130 && matches!(record.state, ExportState::Resolved { .. })
131 && record.symbol == cli_main_entrypoint_symbol(verb)
132}
133
134fn record_claims_generic_cli_main(record: &ExportRecord) -> bool {
135 record.kind == ExportKind::named(ExportKind::FUNCTION)
136 && matches!(record.state, ExportState::Resolved { .. })
137 && record.symbol == Symbol::new(CLI_MAIN_ENTRYPOINT)
138}
139
140fn record_claims_cli_main(record: &ExportRecord) -> bool {
141 record.kind == ExportKind::named(ExportKind::FUNCTION)
142 && matches!(record.state, ExportState::Resolved { .. })
143 && symbol_claims_cli_main(&record.symbol)
144}
145
146fn symbol_claims_cli_main(symbol: &Symbol) -> bool {
147 let symbol = symbol.as_qualified_str();
148 symbol == CLI_MAIN_ENTRYPOINT || symbol.starts_with(&format!("{CLI_MAIN_ENTRYPOINT}/"))
149}
150
151fn no_entrypoint_error(receipts: &[LoadReceipt], verb: Option<&str>) -> CliError {
152 let loaded = if receipts.is_empty() {
153 "none".to_owned()
154 } else {
155 receipts
156 .iter()
157 .map(|receipt| receipt.manifest.id.to_string())
158 .collect::<Vec<_>>()
159 .join(", ")
160 };
161 let requested = verb
162 .map(|verb| format!(" {CLI_MAIN_ENTRYPOINT}/{verb} or {CLI_MAIN_ENTRYPOINT}"))
163 .unwrap_or_else(|| format!(" {CLI_MAIN_ENTRYPOINT}"));
164 CliError::new(format!(
165 "no loaded lib claims{requested}; loaded libs: {loaded}; load one with --load"
166 ))
167}