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