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 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/// Command-line error returned by the bootloader core.
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct CliError {
127    message: String,
128}
129
130impl CliError {
131    /// Builds a command-line error from a user-facing message.
132    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
159/// Returns the version line printed by `sim --version`.
160pub fn version_line() -> String {
161    format!("sim {}\n", env!("CARGO_PKG_VERSION"))
162}
163
164/// Runs the command entry API with process arguments.
165///
166/// This is the one public boot path, expressed through [`Bootloader`]: the default
167/// `sim` runtime is `Bootloader::standard()` (the in-process host loader only), so it
168/// boots no codec or library unless a loadable source is supplied via `--load` or
169/// already cached. A `Boot` command with no available codec returns
170/// `no codec '<name>' available`. To boot a real codec or library in-process, compose
171/// a [`Bootloader`] with [`Bootloader::host_verb`]/[`Bootloader::host_lib`] (or build a
172/// session with [`LoadSession::with_host_factory`]/[`LoadSession::with_loader`] and
173/// call [`run_with_session`]).
174pub 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
182/// Runs the command entry API with an injected loader session.
183pub 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
191/// Runs an already-parsed command with an injected loader session.
192pub 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}