1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3use std::{ffi::OsString, fmt};
30
31mod args;
32mod boot;
33mod bootloader;
34mod codec_boot;
35mod config;
36mod crates_io;
37mod envelope;
38mod exit;
39#[cfg(feature = "registry")]
40mod git_registry;
41mod handoff;
42mod introspect;
43mod load;
44mod receipt;
45mod report;
46mod source;
47
48#[cfg(test)]
49mod codec_boot_tests;
50#[cfg(test)]
51mod config_report_tests;
52#[cfg(test)]
53mod config_tests;
54#[cfg(test)]
55mod handoff_tests;
56#[cfg(test)]
57mod introspect_tests;
58#[cfg(test)]
59mod load_tests;
60#[cfg(test)]
61mod publish_tests;
62#[cfg(test)]
63mod scenario_tests;
64
65pub use args::{CliCommand, parse_args};
66pub use boot::{CliBoot, CliEnvelope, Payload};
67pub use bootloader::Bootloader;
68pub use codec_boot::{DEFAULT_CODEC_NAME, boot_codec_name, codec_lib_symbol};
69pub use config::{
70 ConfigLoadOptions, RuntimeConfigState, load_config_sources, load_config_sources_with_probes,
71 run_config_probe,
72};
73pub use crates_io::{CratesIoResolver, CratesIoSpec, ResolvedCratesIoSource, VersionReq};
74#[cfg(feature = "registry")]
75pub use git_registry::{GIT_REGISTRY_ENDPOINT_ENV, GitRegistryResolver};
76pub use handoff::{CLI_MAIN_ENTRYPOINT, CliEntrypoint, cli_main_entrypoint_symbol};
77pub use load::LoadSession;
78pub use receipt::{LoadReceipt, LoadReceiptRole};
79pub use report::{
80 ConfigReportKind, ConfigReportRequest, ConfigSourceReport, LoadedLibReport, LoadedStateReport,
81 SourceStatus, format_config_sources, format_config_sources_json, format_config_status,
82 format_config_status_json, format_effective_config, format_effective_config_json,
83 render_config_report,
84};
85pub use source::LibSourceSpec;
86
87const HELP: &str = "\
88Usage: sim [OPTIONS] [PAYLOAD...]
89
90Options:
91 --help Print this help text.
92 --version Print the binary version.
93 --codec NAME Select the boot codec name.
94 --load SRC Add a library source to load.
95 --native-audio-provider SRC
96 Try a native audio provider source and degrade if absent.
97 --config-home PATH Read home config from PATH.
98 --config-work PATH Read working config from PATH.
99 --config-file PATH Read one shared config Dir file after root files.
100 --config-site SYMBOL
101 Read a config Dir from a loaded site export.
102 --no-config-files Skip filesystem config discovery.
103 --list Request a loaded-lib list.
104 --inspect SYMBOL Request inspection of a loaded lib or export.
105 config status Report loaded libs, config sources, probes, and diagnostics.
106 config effective LIB
107 Report the effective config table for LIB.
108 config sources Report config source provenance and diagnostics.
109 --json Render a config report command as stable JSON.
110 --eval TEXT Carry eval text for loaded-lib handoff.
111 --script PATH Carry a script path for loaded-lib handoff.
112 --stdin TEXT Carry stdin text for loaded-lib handoff.
113
114Note: the bootloader bakes in no codec. By default it fetches nothing over
115the network and boots only libraries provided via --load (an artifact source) or
116already present in the local cache. A build with the registry feature can fetch from
117an explicit git registry endpoint installed by the host. With no source it reports
118`no codec '<name>' available`.
119";
120
121#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct CliError {
124 message: String,
125}
126
127impl CliError {
128 pub fn new(message: impl Into<String>) -> Self {
130 Self {
131 message: message.into(),
132 }
133 }
134
135 pub(crate) fn unsupported(arg: &str) -> Self {
136 Self::new(format!("unsupported argument: {arg}"))
137 }
138
139 pub(crate) fn missing_value(flag: &str) -> Self {
140 Self::new(format!("{flag} requires a value"))
141 }
142
143 pub(crate) fn duplicate(flag: &str) -> Self {
144 Self::new(format!("{flag} was provided more than once"))
145 }
146}
147
148impl fmt::Display for CliError {
149 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 f.write_str(&self.message)
151 }
152}
153
154impl std::error::Error for CliError {}
155
156pub fn version_line() -> String {
158 format!("sim {}\n", env!("CARGO_PKG_VERSION"))
159}
160
161pub fn run<I, S>(args: I) -> Result<i32, CliError>
172where
173 I: IntoIterator<Item = S>,
174 S: Into<OsString>,
175{
176 Bootloader::standard().run(args)
177}
178
179pub fn run_with_session<I, S>(args: I, session: &mut LoadSession) -> Result<i32, CliError>
181where
182 I: IntoIterator<Item = S>,
183 S: Into<OsString>,
184{
185 run_command_with_session(parse_args(args)?, session)
186}
187
188pub fn run_command_with_session(
190 command: CliCommand,
191 session: &mut LoadSession,
192) -> Result<i32, CliError> {
193 match command {
194 CliCommand::Help => {
195 print!("{HELP}");
196 Ok(0)
197 }
198 CliCommand::Version => {
199 print!("{}", version_line());
200 Ok(0)
201 }
202 CliCommand::Boot(boot) => session.run_loaded_boot(&boot),
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn version_line_uses_package_version() {
212 assert_eq!(
213 version_line(),
214 format!("sim {}\n", env!("CARGO_PKG_VERSION"))
215 );
216 }
217
218 #[test]
219 fn direct_payload_enters_loaded_boot() {
220 let err = run(["sim", "run"]).unwrap_err();
221 assert!(err.to_string().starts_with("no codec 'lisp' available"));
222 }
223}