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