Skip to main content

sim_run_core/
lib.rs

1#![forbid(unsafe_code)]
2#![deny(missing_docs)]
3//! Core command entry API for the SIM bootloader.
4//!
5//! # Bootloader frame
6//!
7//! The shipped `sim` binary is a **bootloader frame, not a batteries-included
8//! runtime**. [`run`] builds a [`LoadSession`] whose only registered loader is
9//! the in-process [`LoadSession::add_host_factory`] host loader: with no host
10//! factory and no injected artifact loader it can boot **no codec and no
11//! library**, so `run(["sim", "run"])` fails with `no codec 'lisp' available`.
12//! This is by design: behavior lives in loadable libraries, not baked into the
13//! frame. The default frame loads a codec when an explicit source, cache
14//! artifact, registry resolver, or host factory supplies it.
15//!
16//! A working session therefore comes from one of:
17//!
18//! - an explicitly provided source: `--load path/to/artifact.simlib` (needs an
19//!   artifact loader registered via [`LoadSession::with_loader`]), or
20//! - a seeded cache resolved by the cache-only [`CratesIoResolver`] (it never
21//!   reaches the network unless an explicit registry resolver is installed; the
22//!   cache must otherwise already hold the artifact), or
23//! - a host factory registered through [`LoadSession::with_host_factory`] and
24//!   driven via [`run_with_session`] -- the path every functional test uses.
25//!
26//! The `registry` feature adds a git registry artifact resolver, but it is active
27//! only when the host installs it. Nothing here bakes in a codec.
28
29use 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/// Command-line error returned by the bootloader core.
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct CliError {
139    message: String,
140}
141
142impl CliError {
143    /// Builds a command-line error from a user-facing message.
144    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
171/// Returns the version line printed by `sim --version`.
172pub fn version_line() -> String {
173    format!("sim {}\n", env!("CARGO_PKG_VERSION"))
174}
175
176/// Runs the command entry API with process arguments.
177///
178/// This is the one public boot path, expressed through [`Bootloader`]: the default
179/// `sim` runtime is `Bootloader::standard()` (the in-process host loader only), so it
180/// boots no codec or library unless a loadable source is supplied via `--load` or
181/// already cached. A `Boot` command with no available codec returns
182/// `no codec '<name>' available`. To boot a real codec or library in-process, compose
183/// a [`Bootloader`] with [`Bootloader::host_verb`]/[`Bootloader::host_lib`] (or build a
184/// session with [`LoadSession::with_host_factory`]/[`LoadSession::with_loader`] and
185/// call [`run_with_session`]).
186pub 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
194/// Runs the command entry API with an injected loader session.
195pub 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
203/// Runs an already-parsed command with an injected loader session.
204pub 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}