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