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};
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/// Command-line error returned by the bootloader core.
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct CliError {
138    message: String,
139}
140
141impl CliError {
142    /// Builds a command-line error from a user-facing message.
143    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
170/// Returns the version line printed by `sim --version`.
171pub fn version_line() -> String {
172    format!("sim {}\n", env!("CARGO_PKG_VERSION"))
173}
174
175/// Runs the command entry API with process arguments.
176///
177/// This is the one public boot path, expressed through [`Bootloader`]: the default
178/// `sim` runtime is `Bootloader::standard()` (the in-process host loader only), so it
179/// boots no codec or library unless a loadable source is supplied via `--load` or
180/// already cached. A `Boot` command with no available codec returns
181/// `no codec '<name>' available`. To boot a real codec or library in-process, compose
182/// a [`Bootloader`] with [`Bootloader::host_verb`]/[`Bootloader::host_lib`] (or build a
183/// session with [`LoadSession::with_host_factory`]/[`LoadSession::with_loader`] and
184/// call [`run_with_session`]).
185pub 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
193/// Runs the command entry API with an injected loader session.
194pub 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
202/// Runs an already-parsed command with an injected loader session.
203pub 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}