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