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};
88#[cfg(feature = "registry")]
89pub use git_registry::{GIT_REGISTRY_ENDPOINT_ENV, GitRegistryResolver};
90pub use handoff::{CLI_MAIN_ENTRYPOINT, CliEntrypoint, cli_main_entrypoint_symbol};
91pub use load::LoadSession;
92pub use receipt::{LoadReceipt, LoadReceiptRole};
93pub use report::{
94 ConfigReportKind, ConfigReportRequest, ConfigSourceReport, LoadedLibReport, LoadedStateReport,
95 SourceStatus, format_config_sources, format_config_sources_json, format_config_status,
96 format_config_status_json, format_effective_config, format_effective_config_json,
97 render_config_report,
98};
99pub use source::LibSourceSpec;
100
101const HELP: &str = "\
102Usage: sim [OPTIONS] [PAYLOAD...]
103
104Options:
105 --help Print this help text.
106 --version Print the binary version.
107 --codec NAME Select the boot codec name.
108 --load SRC Add a library source to load.
109 --native-audio-provider SRC
110 Try a native audio provider source and degrade if absent.
111 --config-home PATH Read home config from PATH.
112 --config-work PATH Read working config from PATH.
113 --config-file PATH Read one shared config Dir file after root files.
114 --config-site SYMBOL
115 Read a config Dir from a loaded site export.
116 --no-config-files Skip filesystem config discovery.
117 --list Request a loaded-lib list.
118 --inspect SYMBOL Request inspection of a loaded lib or export.
119 config status Report loaded libs, config sources, probes, and diagnostics.
120 config effective LIB
121 Report the effective config table for LIB.
122 config sources Report config source provenance and diagnostics.
123 --json Render a config report command as stable JSON.
124 --eval TEXT Carry eval text for loaded-lib handoff.
125 --script PATH Carry a script path for loaded-lib handoff.
126 --stdin TEXT Carry stdin text for loaded-lib handoff.
127
128Note: the bootloader bakes in no codec. By default it fetches nothing over
129the network and boots only libraries provided via --load (an artifact source) or
130already present in the local cache. A build with the registry feature can fetch from
131an explicit git registry endpoint installed by the host. With no source it reports
132`no codec '<name>' available`.
133";
134
135#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct CliError {
138 message: String,
139}
140
141impl CliError {
142 pub fn new(message: impl Into<String>) -> Self {
144 Self {
145 message: message.into(),
146 }
147 }
148
149 pub(crate) fn unsupported(arg: &str) -> Self {
150 Self::new(format!("unsupported argument: {arg}"))
151 }
152
153 pub(crate) fn missing_value(flag: &str) -> Self {
154 Self::new(format!("{flag} requires a value"))
155 }
156
157 pub(crate) fn duplicate(flag: &str) -> Self {
158 Self::new(format!("{flag} was provided more than once"))
159 }
160}
161
162impl fmt::Display for CliError {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 f.write_str(&self.message)
165 }
166}
167
168impl std::error::Error for CliError {}
169
170pub fn version_line() -> String {
172 format!("sim {}\n", env!("CARGO_PKG_VERSION"))
173}
174
175pub fn run<I, S>(args: I) -> Result<i32, CliError>
186where
187 I: IntoIterator<Item = S>,
188 S: Into<OsString>,
189{
190 Bootloader::standard().run(args)
191}
192
193pub fn run_with_session<I, S>(args: I, session: &mut LoadSession) -> Result<i32, CliError>
195where
196 I: IntoIterator<Item = S>,
197 S: Into<OsString>,
198{
199 run_command_with_session(parse_args(args)?, session)
200}
201
202pub fn run_command_with_session(
204 command: CliCommand,
205 session: &mut LoadSession,
206) -> Result<i32, CliError> {
207 match command {
208 CliCommand::Help => {
209 print!("{HELP}");
210 Ok(0)
211 }
212 CliCommand::Version => {
213 print!("{}", version_line());
214 Ok(0)
215 }
216 CliCommand::Boot(boot) => session.run_loaded_boot(&boot),
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn version_line_uses_package_version() {
226 assert_eq!(
227 version_line(),
228 format!("sim {}\n", env!("CARGO_PKG_VERSION"))
229 );
230 }
231
232 #[test]
233 fn direct_payload_enters_loaded_boot() {
234 let err = run(["sim", "run"]).unwrap_err();
235 assert!(err.to_string().starts_with("no codec 'lisp' available"));
236 }
237}