inoculate/
lib.rs

1use clap::{ArgGroup, Args, Parser, ValueHint};
2use color_eyre::{
3    eyre::{format_err, WrapErr},
4    Help,
5};
6use std::{
7    fmt,
8    path::{Path, PathBuf},
9};
10
11pub use color_eyre::eyre::Result;
12pub mod cargo;
13pub mod gdb;
14pub mod qemu;
15pub mod term;
16pub mod trace;
17
18#[derive(Debug, Parser)]
19#[clap(about, version, author = "Eliza Weisman <eliza@elizas.website>")]
20pub struct Options {
21    /// Which command to run?
22    ///
23    /// By default, an image is built but not run.
24    #[clap(subcommand)]
25    pub cmd: Option<Subcommand>,
26
27    /// The path to the kernel binary.
28    #[clap(value_hint = ValueHint::FilePath)]
29    pub kernel_bin: PathBuf,
30
31    /// The path to the kernel's Cargo manifest. If this is not
32    /// provided, it will be located automatically.
33    #[clap(value_hint = ValueHint::FilePath)]
34    #[clap(long, global = true)]
35    pub kernel_manifest: Option<PathBuf>,
36
37    /// Overrides the directory in which to build the output image.
38    #[clap(short, long, env = "OUT_DIR", value_hint = ValueHint::DirPath, global = true)]
39    pub out_dir: Option<PathBuf>,
40
41    /// Overrides the target directory for the kernel build.
42    #[clap(
43        short,
44        long,
45        env = "CARGO_TARGET_DIR",
46        value_hint = ValueHint::DirPath,
47        global = true
48    )]
49    pub target_dir: Option<PathBuf>,
50
51    /// Overrides the path to the `cargo` executable.
52    ///
53    /// By default, this is read from the `CARGO` environment variable.
54    #[clap(
55        long = "cargo",
56        env = "CARGO",
57        default_value = "cargo",
58        value_hint = ValueHint::ExecutablePath,
59        global = true
60    )]
61    pub cargo_path: PathBuf,
62
63    #[clap(flatten)]
64    pub output: term::OutputOptions,
65
66    /// Configures the bootloader.
67    #[clap(flatten)]
68    pub bootloader: BootloaderOptions,
69}
70
71#[derive(Clone, Debug, Args)]
72#[command(
73    next_help_heading = "Bootloader Options",
74    group = ArgGroup::new(Self::ARG_GROUP).multiple(true),
75)]
76pub struct BootloaderOptions {
77    /// How to boot Mycelium.
78    ///
79    /// This determines which type of image is built, and (if a QEMU subcommand
80    /// is executed) how QEMU will boot Mycelium.
81    #[clap(
82        long = "boot",
83        short = 'b',
84        default_value_t = BootMode::Uefi,
85        global = true,
86        group = Self::ARG_GROUP,
87    )]
88    pub mode: BootMode,
89
90    /// Log level for the bootloader.
91    #[clap(
92        long,
93        default_value_t = BootLogLevel::Info,
94        global = true,
95        group = Self::ARG_GROUP,
96    )]
97    boot_log: BootLogLevel,
98
99    /// Instructs the bootloader to set up a framebuffer format that has at least the given height.
100    ///
101    /// If this is not possible, the bootloader will fall back to a smaller format.
102    #[clap(long, global = true, group = Self::ARG_GROUP)]
103    framebuffer_height: Option<u64>,
104
105    /// Instructs the bootloader to set up a framebuffer format that has at least the given width.
106    ///
107    /// If this is not possible, the bootloader will fall back to a smaller format.
108    #[clap(long, global = true, group = Self::ARG_GROUP)]
109    framebuffer_width: Option<u64>,
110}
111
112#[derive(Copy, Clone, Debug, Eq, PartialEq, clap::ValueEnum)]
113#[repr(u8)]
114#[clap(rename_all = "upper")]
115pub enum BootMode {
116    /// Boot mycelium using the UEFI bootloader.
117    ///
118    /// The kernel image will be output to `<OUT_DIR>/uefi.img`.
119    Uefi,
120    /// Boot mycelium using the legacy BIOS bootloader.
121    ///
122    /// The kernel image will be output to `<OUT_DIR>/bios.img`.
123    Bios,
124}
125
126#[derive(Debug, Parser)]
127pub enum Subcommand {
128    #[clap(flatten)]
129    Qemu(qemu::Cmd),
130    /// Run `gdb` without launching the kernel in QEMU.
131    ///
132    /// This assumes QEMU was already started by a separate `cargo inoculate`
133    /// invocation, and that invocation was configured to listen for a GDB
134    /// connection on the default port.
135    Gdb,
136}
137
138#[derive(Debug)]
139pub struct Paths {
140    pub pwd: PathBuf,
141    pub kernel_bin: PathBuf,
142    pub kernel_manifest: PathBuf,
143    pub out_dir: PathBuf,
144}
145
146#[derive(Copy, Clone, Debug, Eq, PartialEq, clap::ValueEnum)]
147#[repr(u8)]
148enum BootLogLevel {
149    /// A level lower than all log levels.
150    Off,
151    /// Corresponds to the `Error` log level.
152    Error,
153    /// Corresponds to the `Warn` log level.
154    Warn,
155    /// Corresponds to the `Info` log level.
156    Info,
157    /// Corresponds to the `Debug` log level.
158    Debug,
159    /// Corresponds to the `Trace` log level.
160    Trace,
161}
162
163impl Subcommand {
164    pub fn run(&self, image: &Path, paths: &Paths, boot: BootMode) -> Result<()> {
165        match self {
166            Subcommand::Qemu(qemu) => qemu.run_qemu(image, paths, boot),
167            Subcommand::Gdb => crate::gdb::run_gdb(paths.kernel_bin(), 1234).map(|_| ()),
168        }
169    }
170}
171
172impl Options {
173    pub fn is_test(&self) -> bool {
174        matches!(self.cmd, Some(Subcommand::Qemu(qemu::Cmd::Test { .. })))
175    }
176
177    pub fn wheres_the_kernel(&self) -> Result<PathBuf> {
178        tracing::debug!("where's the kernel?");
179        if let Some(path) = self.kernel_manifest.as_ref() {
180            tracing::info!(path = %path.display(), "kernel manifest path path overridden");
181            return Ok(path.clone());
182        }
183        locate_cargo_manifest::locate_manifest()
184            .note("where the hell is the kernel's Cargo.toml?")
185            .note("this should never happen, seriously, wtf")
186            .suggestion("have you tried not having it be missing")
187    }
188
189    pub fn wheres_the_kernel_bin(&self) -> Result<PathBuf> {
190        tracing::debug!("where's the kernel binary?");
191        self.kernel_bin
192            // the bootloader crate's build script gets mad if this is a
193            // relative path
194            .canonicalize()
195            .context("couldn't to canonicalize kernel manifest path")
196            .note("it should work")
197    }
198
199    pub fn paths(&self) -> Result<Paths> {
200        let kernel_manifest = self.wheres_the_kernel()?;
201        tracing::info!(path = %kernel_manifest.display(), "found kernel manifest");
202
203        let kernel_bin = self.wheres_the_kernel_bin()?;
204        tracing::info!(path = %kernel_bin.display(), "found kernel binary");
205
206        let pwd = std::env::current_dir().unwrap_or_else(|error| {
207            tracing::warn!(?error, "error getting current dir");
208            Default::default()
209        });
210        tracing::debug!(path = %pwd.display(), "found pwd");
211
212        let out_dir = self
213            .out_dir
214            .as_ref()
215            .map(|path| path.as_ref())
216            .or_else(|| kernel_bin.parent())
217            .ok_or_else(|| format_err!("can't find out dir, wtf"))
218            .context("determining out dir")
219            .note("somethings messed up lol")?
220            .to_path_buf();
221        tracing::debug!(path = %out_dir.display(), "determined output directory");
222
223        Ok(Paths {
224            kernel_manifest,
225            kernel_bin,
226            pwd,
227            out_dir,
228        })
229    }
230
231    pub fn make_image(&self, paths: &Paths) -> Result<PathBuf> {
232        let _span = tracing::info_span!("make_image").entered();
233
234        tracing::info!(
235            img = %paths.relative(paths.kernel_bin()).display(),
236            boot = %self.bootloader.mode,
237            "Building kernel disk image",
238        );
239
240        let bootcfg = self.bootloader.boot_config();
241        let path = match self.bootloader.mode {
242            BootMode::Uefi => {
243                let path = paths.uefi_img();
244                let mut builder = bootloader::UefiBoot::new(paths.kernel_bin());
245                builder.set_boot_config(&bootcfg);
246                builder
247                    .create_disk_image(&path)
248                    .map_err(|error| format_err!("failed to build UEFI image: {error}"))
249                    .with_note(|| format!("output path: {}", path.display()))?;
250                path
251            }
252            BootMode::Bios => {
253                let path = paths.bios_img();
254                let mut builder = bootloader::BiosBoot::new(paths.kernel_bin());
255                builder.set_boot_config(&bootcfg);
256                builder
257                    .create_disk_image(&path)
258                    .map_err(|error| format_err!("failed to build BIOS image: {error}"))
259                    .with_note(|| format!("output path: {}", path.display()))?;
260                path
261            }
262        };
263
264        tracing::info!(
265            "Created bootable disk image ({})",
266            paths.relative(&path).display()
267        );
268
269        Ok(path)
270    }
271}
272
273// === impl Paths ===
274
275impl Paths {
276    pub fn kernel_bin(&self) -> &Path {
277        self.kernel_bin.as_ref()
278    }
279
280    pub fn kernel_manifest(&self) -> &Path {
281        self.kernel_manifest.as_ref()
282    }
283
284    pub fn pwd(&self) -> &Path {
285        self.pwd.as_ref()
286    }
287
288    pub fn uefi_img(&self) -> PathBuf {
289        self.out_dir.join("uefi.img")
290    }
291
292    pub fn bios_img(&self) -> PathBuf {
293        self.out_dir.join("bios.img")
294    }
295
296    pub fn relative<'path>(&self, path: &'path Path) -> &'path Path {
297        path.strip_prefix(self.pwd()).unwrap_or(path)
298    }
299}
300
301// === impl BootloaderOptions ===
302
303impl BootloaderOptions {
304    const ARG_GROUP: &'static str = "boot-opts";
305
306    fn boot_config(&self) -> bootloader_boot_config::BootConfig {
307        let mut bootcfg = bootloader::BootConfig::default();
308        bootcfg.log_level = self.boot_log.into();
309        bootcfg.frame_buffer_logging = true;
310        bootcfg.serial_logging = true;
311        if self.framebuffer_height.is_some() {
312            bootcfg.frame_buffer.minimum_framebuffer_height = self.framebuffer_height;
313        }
314        if self.framebuffer_width.is_some() {
315            bootcfg.frame_buffer.minimum_framebuffer_width = self.framebuffer_width;
316        }
317        tracing::debug!(
318            ?bootcfg.log_level,
319            bootcfg.frame_buffer_logging,
320            bootcfg.serial_logging,
321            ?bootcfg.frame_buffer
322        );
323        bootcfg
324    }
325}
326
327// === impl BootMode ===
328
329impl fmt::Display for BootMode {
330    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
331        match self {
332            BootMode::Uefi => f.pad("UEFI"),
333            BootMode::Bios => f.pad("BIOS"),
334        }
335    }
336}
337
338// === impl BootLogLevel ===
339
340impl fmt::Display for BootLogLevel {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        match self {
343            Self::Off => "off",
344            Self::Error => "error",
345            Self::Warn => "warn",
346            Self::Info => "info",
347            Self::Debug => "debug",
348            Self::Trace => "trace",
349        }
350        .fmt(f)
351    }
352}
353
354impl From<BootLogLevel> for bootloader_boot_config::LevelFilter {
355    fn from(level: BootLogLevel) -> bootloader_boot_config::LevelFilter {
356        use bootloader_boot_config::LevelFilter;
357        match level {
358            BootLogLevel::Off => LevelFilter::Off,
359            BootLogLevel::Error => LevelFilter::Error,
360            BootLogLevel::Warn => LevelFilter::Warn,
361            BootLogLevel::Info => LevelFilter::Info,
362            BootLogLevel::Debug => LevelFilter::Debug,
363            BootLogLevel::Trace => LevelFilter::Trace,
364        }
365    }
366}