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