diff --git a/README.en.md b/README.en.md index a2ab834c..fda802ba 100644 --- a/README.en.md +++ b/README.en.md @@ -149,6 +149,28 @@ ostool board run --package paging-test --bin basic > Exit shortcut: In the serial terminal (e.g., `ostool run uboot`), press `Ctrl+A` then `x` to quit; the tool captures this sequence and exits gracefully instead of sending it to the target device. > For more keyboard mappings, see `ostool/src/sterm/mod.rs`. +#### 5. Prepare Boot Artifacts + +```bash +# Build the kernel and prepare a boot package under target/boot +ostool boot prepare + +# Include a DTB and temporarily override Cargo package / binary target +ostool boot prepare --dtb virt.dtb --package paging-test --bin basic + +# Override FIT addresses. Addresses accept decimal or 0x-prefixed hexadecimal. +ostool boot prepare --kernel-load-addr 0x80200000 --kernel-entry-addr 0x80200000 + +# Prepare the manifest, ELF metadata, boot script, and staging directories without FIT +ostool boot prepare --no-fit +``` + +`boot prepare` treats the built ELF as the canonical source artifact and derives +ELF entry/load metadata automatically. When FIT generation is enabled, the +required BIN is derived automatically; users do not need to declare a binary +artifact manually. The default output directory is the invocation's `target/boot`, +and the command writes a stable `boot-artifacts.json` v1 manifest. + ## ⚙️ Configuration Files ostool uses multiple independent TOML configuration files, each responsible for different functional modules: @@ -201,8 +223,15 @@ pre_build_cmds = ["make prepare"] # Post-build commands post_build_cmds = ["make post-process"] -# Output as binary file -to_bin = true +# Optional compatibility field. U-Boot, board, and UEFI QEMU runs prepare the +# required BIN automatically. +to_bin = false + +# Optional analysis artifacts. Field names describe artifacts, not tool names. +[system.Cargo.artifacts] +disassembly = false +elf_info = false +symbols = false ``` Command-line `--package`/`--bin` overrides are applied to the final Cargo @@ -223,8 +252,14 @@ build_cmd = "make ARCH=aarch64 A=examples/helloworld" # Generated ELF file path elf_path = "examples/helloworld/helloworld_aarch64-qemu-virt.elf" -# Output as binary file -to_bin = true +# Optional compatibility field. U-Boot, board, and UEFI QEMU runs prepare the +# required BIN automatically. +to_bin = false + +[system.Custom.artifacts] +disassembly = false +elf_info = false +symbols = false ``` ### QEMU Configuration (.qemu.toml) @@ -238,14 +273,21 @@ args = ["-machine", "virt", "-cpu", "cortex-a57", "-nographic"] # Enable UEFI boot uefi = false -# Output as binary file -to_bin = true +# Optional compatibility field. UEFI QEMU prepares the required BIN automatically. +to_bin = false # Success regex patterns (for auto-detection) success_regex = ["Hello from my OS", "Kernel booted successfully"] # Failure regex patterns (for auto-detection) fail_regex = ["panic", "error", "failed"] + +# Boot mode. direct is the default path; uboot is recognized and fails early +# when firmware is missing. +[boot] +mode = "direct" +# mode = "uboot" +# firmware = "target/firmware/u-boot.bin" ``` ### U-Boot Configuration (.uboot.toml) diff --git a/README.md b/README.md index e38ddd5f..c12e855d 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,26 @@ ostool board run --package paging-test --bin basic > 交互退出:在串口终端(如 `ostool run uboot`)中,按下 `Ctrl+A` 后再按 `x`,工具会检测到该序列并优雅退出,不会将按键发送到目标设备。 > 更多键盘快捷键映射可参考源码 `ostool/src/sterm/mod.rs`。 +#### 5. 准备启动产物 + +```bash +# 构建内核并准备 target/boot 下的启动产物包 +ostool boot prepare + +# 附带 DTB,并临时覆盖 Cargo package / binary target +ostool boot prepare --dtb virt.dtb --package paging-test --bin basic + +# 覆盖 FIT 地址,地址支持十进制或 0x 前缀十六进制 +ostool boot prepare --kernel-load-addr 0x80200000 --kernel-entry-addr 0x80200000 + +# 只准备 manifest、ELF metadata、boot script 和 staging 目录,不生成 FIT +ostool boot prepare --no-fit +``` + +`boot prepare` 以构建出的 ELF 作为唯一源产物,自动推导 ELF entry/load metadata;需要 +FIT 时会自动派生 BIN,不需要在配置中手动声明 binary。默认输出目录为当前 invocation 的 +`target/boot`,并写出 `boot-artifacts.json` v1 manifest。 + ## ⚙️ 配置文件 ostool 使用多个独立的 TOML 配置文件,每个文件负责不同的功能模块: @@ -202,6 +222,12 @@ post_build_cmds = ["make post-process"] # 可选兼容字段。U-Boot、board 和 UEFI QEMU 运行会自动准备所需 BIN。 to_bin = false + +# 可选分析产物。字段名描述产物,不描述底层命令。 +[system.Cargo.artifacts] +disassembly = false +elf_info = false +symbols = false ``` 命令行 `--package`/`--bin` 会先覆盖 `.build.toml` 中的 Cargo 包/二进制选择,再用于 @@ -223,6 +249,11 @@ elf_path = "examples/helloworld/helloworld_aarch64-qemu-virt.elf" # 可选兼容字段。U-Boot、board 和 UEFI QEMU 运行会自动准备所需 BIN。 to_bin = false + +[system.Custom.artifacts] +disassembly = false +elf_info = false +symbols = false ``` ### QEMU 配置 (.qemu.toml) @@ -244,6 +275,12 @@ success_regex = ["Hello from my OS", "Kernel booted successfully"] # 失败运行的正则表达式(用于自动检测) fail_regex = ["panic", "error", "failed"] + +# 启动模式。direct 为默认路径;uboot 目前只识别配置并在缺少 firmware 时提前报错。 +[boot] +mode = "direct" +# mode = "uboot" +# firmware = "target/firmware/u-boot.bin" ``` ### U-Boot 配置 (.uboot.toml) diff --git a/ostool/src/artifact/analysis.rs b/ostool/src/artifact/analysis.rs new file mode 100644 index 00000000..8ebb7f2a --- /dev/null +++ b/ostool/src/artifact/analysis.rs @@ -0,0 +1,278 @@ +//! Optional analysis artifact generation for prepared ELF files. + +use std::{ + ffi::OsString, + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, bail}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::{ + artifact::{ + object_tools::{ObjectToolKind, ObjectTools}, + state::{DebugArtifactKind, OutputArtifacts}, + }, + process::{self, ProcessContext}, + utils::PathResultExt, +}; + +/// Optional analysis artifacts derived from the prepared runtime ELF. +#[derive(Default, Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct AnalysisArtifactConfig { + /// Generate a disassembly text artifact. + #[serde(default, skip_serializing_if = "is_false")] + pub disassembly: bool, + /// Generate an ELF information text artifact. + #[serde(default, skip_serializing_if = "is_false")] + pub elf_info: bool, + /// Generate a symbol table text artifact. + #[serde(default, skip_serializing_if = "is_false")] + pub symbols: bool, +} + +impl AnalysisArtifactConfig { + pub(crate) fn is_empty(&self) -> bool { + !self.disassembly && !self.elf_info && !self.symbols + } +} + +fn is_false(value: &bool) -> bool { + !*value +} + +pub(crate) fn generate_analysis_artifacts( + context: &ProcessContext, + elf_path: &Path, + output_dir: &Path, + config: &AnalysisArtifactConfig, + tools: &ObjectTools, + artifacts: &mut OutputArtifacts, +) -> anyhow::Result<()> { + if config.is_empty() { + return Ok(()); + } + + fs::create_dir_all(output_dir).with_path("failed to create directory", output_dir)?; + + if config.disassembly { + run_analysis_tool( + context, + tools, + elf_path, + output_dir, + artifacts, + AnalysisToolSpec { + tool: ObjectToolKind::Objdump, + args: vec![OsString::from("-d"), elf_path.as_os_str().to_os_string()], + kind: DebugArtifactKind::Disassembly, + }, + )?; + } + + if config.elf_info { + run_analysis_tool( + context, + tools, + elf_path, + output_dir, + artifacts, + AnalysisToolSpec { + tool: ObjectToolKind::Readobj, + args: vec![ + OsString::from("--file-headers"), + OsString::from("--program-headers"), + OsString::from("--sections"), + OsString::from("--symbols"), + elf_path.as_os_str().to_os_string(), + ], + kind: DebugArtifactKind::ElfInfo, + }, + )?; + } + + if config.symbols { + run_analysis_tool( + context, + tools, + elf_path, + output_dir, + artifacts, + AnalysisToolSpec { + tool: ObjectToolKind::Nm, + args: vec![OsString::from("-n"), elf_path.as_os_str().to_os_string()], + kind: DebugArtifactKind::Symbols, + }, + )?; + } + + Ok(()) +} + +struct AnalysisToolSpec { + tool: ObjectToolKind, + args: Vec, + kind: DebugArtifactKind, +} + +fn run_analysis_tool( + context: &ProcessContext, + tools: &ObjectTools, + elf_path: &Path, + output_dir: &Path, + artifacts: &mut OutputArtifacts, + spec: AnalysisToolSpec, +) -> anyhow::Result<()> { + let output_path = analysis_output_path(elf_path, output_dir, spec.kind)?; + let mut command = process::command(tools.program(spec.tool), context); + command.args(spec.args); + command.print_cmd(); + let output = command + .output() + .with_context(|| format!("failed to run analysis tool for {}", elf_path.display()))?; + if !output.status.success() { + bail!( + "analysis tool failed with status {}: {}", + output.status, + String::from_utf8_lossy(&output.stderr) + ); + } + fs::write(&output_path, output.stdout).with_path("failed to write file", &output_path)?; + artifacts.register_debug_artifact(spec.kind, output_path); + Ok(()) +} + +fn analysis_output_path( + elf_path: &Path, + output_dir: &Path, + kind: DebugArtifactKind, +) -> anyhow::Result { + let stem = elf_path + .file_stem() + .ok_or_else(|| anyhow!("invalid ELF file path: {}", elf_path.display()))? + .to_string_lossy(); + let suffix = match kind { + DebugArtifactKind::Disassembly => "disassembly.txt", + DebugArtifactKind::ElfInfo => "elf-info.txt", + DebugArtifactKind::Symbols => "symbols.txt", + }; + Ok(output_dir.join(format!("{stem}.{suffix}"))) +} + +#[cfg(test)] +mod tests { + use std::{fs, path::PathBuf}; + + use crate::{ + artifact::{ + analysis::{AnalysisArtifactConfig, generate_analysis_artifacts}, + object_tools::ObjectTools, + state::{DebugArtifactKind, OutputArtifacts}, + }, + process::ProcessContext, + project::{resolve_project_layout, variables::VariableScope}, + }; + + fn process_context(root: &std::path::Path) -> ProcessContext { + fs::write( + root.join("Cargo.toml"), + "[package]\nname = \"sample\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + fs::create_dir_all(root.join("src")).unwrap(); + fs::write(root.join("src/lib.rs"), "").unwrap(); + + let layout = resolve_project_layout(Some(root.to_path_buf())).unwrap(); + let scope = VariableScope::for_package(&layout, root.to_path_buf()); + ProcessContext::new(root.to_path_buf(), root.to_path_buf(), scope, None) + } + + fn fake_tool(root: &std::path::Path, name: &str, body: &str) -> PathBuf { + let script = root.join(name); + fs::write(&script, body).unwrap(); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mut permissions = fs::metadata(&script).unwrap().permissions(); + permissions.set_mode(0o755); + fs::set_permissions(&script, permissions).unwrap(); + } + script + } + + #[test] + fn generates_requested_analysis_outputs() { + let temp = tempfile::tempdir().unwrap(); + let context = process_context(temp.path()); + let elf = temp.path().join("kernel.elf"); + fs::write(&elf, "elf").unwrap(); + let out = temp.path().join("analysis"); + let mut artifacts = OutputArtifacts::default(); + + fake_tool( + temp.path(), + "rust-objdump", + "#!/bin/sh\nprintf 'disassembly:%s\\n' \"$@\"\n", + ); + fake_tool( + temp.path(), + "rust-readobj", + "#!/bin/sh\nprintf 'elf-info:%s\\n' \"$@\"\n", + ); + fake_tool( + temp.path(), + "rust-nm", + "#!/bin/sh\nprintf 'symbols:%s\\n' \"$@\"\n", + ); + + let old_path = std::env::var_os("PATH").unwrap_or_default(); + let new_path = format!("{}:{}", temp.path().display(), old_path.to_string_lossy()); + unsafe { + std::env::set_var("PATH", new_path); + } + + generate_analysis_artifacts( + &context, + &elf, + &out, + &AnalysisArtifactConfig { + disassembly: true, + elf_info: true, + symbols: true, + }, + &ObjectTools, + &mut artifacts, + ) + .unwrap(); + + unsafe { + std::env::set_var("PATH", old_path); + } + + let disassembly = artifacts + .debug_artifacts() + .get(DebugArtifactKind::Disassembly) + .unwrap(); + let elf_info = artifacts + .debug_artifacts() + .get(DebugArtifactKind::ElfInfo) + .unwrap(); + let symbols = artifacts + .debug_artifacts() + .get(DebugArtifactKind::Symbols) + .unwrap(); + + assert_eq!(disassembly, out.join("kernel.disassembly.txt").as_path()); + assert_eq!(elf_info, out.join("kernel.elf-info.txt").as_path()); + assert_eq!(symbols, out.join("kernel.symbols.txt").as_path()); + assert!(fs::read_to_string(disassembly).unwrap().contains("-d")); + assert!( + fs::read_to_string(elf_info) + .unwrap() + .contains("--file-headers") + ); + assert!(fs::read_to_string(symbols).unwrap().contains("-n")); + } +} diff --git a/ostool/src/artifact/elf_metadata.rs b/ostool/src/artifact/elf_metadata.rs new file mode 100644 index 00000000..cf1a8179 --- /dev/null +++ b/ostool/src/artifact/elf_metadata.rs @@ -0,0 +1,97 @@ +//! ELF-derived boot metadata. + +use std::{fs, path::Path}; + +use anyhow::Context as _; +use object::{Object, ObjectSegment, ObjectSymbol}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::utils::PathResultExt; + +/// Boot-relevant metadata derived from the prepared kernel ELF. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ElfBootMetadata { + /// Object crate architecture name for the ELF file. + pub architecture: String, + /// ELF entry point. + pub entry: u64, + /// Load address used by boot artifact generation. + pub load: u64, + /// Address of `__executable_start`, when present. + #[serde(skip_serializing_if = "Option::is_none")] + pub executable_start: Option, + /// First loadable segment, when the object exposes one. + #[serde(skip_serializing_if = "Option::is_none")] + pub first_load_segment: Option, +} + +/// Loadable segment summary used as boot metadata evidence. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct ElfLoadSegment { + pub address: u64, + pub size: u64, +} + +pub(crate) fn read_elf_boot_metadata(path: &Path) -> anyhow::Result { + let data = fs::read(path).with_path("failed to read ELF file", path)?; + let file = object::File::parse(data.as_slice()) + .with_context(|| format!("failed to parse ELF file: {}", path.display()))?; + let entry = file.entry(); + let executable_start = find_executable_start(&file); + let first_load_segment = file + .segments() + .filter(|segment| segment.size() > 0) + .min_by_key(ObjectSegment::address) + .map(|segment| ElfLoadSegment { + address: segment.address(), + size: segment.size(), + }); + let load = executable_start + .or_else(|| first_load_segment.as_ref().map(|segment| segment.address)) + .unwrap_or(entry); + + Ok(ElfBootMetadata { + architecture: format!("{:?}", file.architecture()), + entry, + load, + executable_start, + first_load_segment, + }) +} + +fn find_executable_start(file: &object::File<'_>) -> Option { + file.symbols().find_map(|symbol| { + let name = symbol.name().ok()?; + (name == "__executable_start").then_some(symbol.address()) + }) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use crate::artifact::elf_metadata::read_elf_boot_metadata; + + #[test] + fn reads_boot_metadata_from_current_executable() { + let temp = tempfile::tempdir().unwrap(); + let source = std::env::current_exe().unwrap(); + let elf = temp.path().join("sample-elf"); + fs::copy(source, &elf).unwrap(); + + let metadata = read_elf_boot_metadata(&elf).unwrap(); + + assert!(!metadata.architecture.is_empty()); + assert_eq!( + metadata.load, + metadata + .executable_start + .or_else(|| metadata + .first_load_segment + .as_ref() + .map(|segment| segment.address)) + .unwrap_or(metadata.entry) + ); + } +} diff --git a/ostool/src/artifact/mod.rs b/ostool/src/artifact/mod.rs index d5359050..693fdfe7 100644 --- a/ostool/src/artifact/mod.rs +++ b/ostool/src/artifact/mod.rs @@ -1,5 +1,7 @@ //! Runtime artifact preparation and state tracking. +pub(crate) mod analysis; +pub(crate) mod elf_metadata; pub(crate) mod object_tools; pub(crate) mod runtime; pub(crate) mod state; diff --git a/ostool/src/artifact/object_tools.rs b/ostool/src/artifact/object_tools.rs index 06c89d08..ddfe9c53 100644 --- a/ostool/src/artifact/object_tools.rs +++ b/ostool/src/artifact/object_tools.rs @@ -35,6 +35,21 @@ impl ObjectTools { pub(crate) fn objcopy(&self) -> PathBuf { self.program(ObjectToolKind::Objcopy) } + + #[allow(dead_code)] + pub(crate) fn objdump(&self) -> PathBuf { + self.program(ObjectToolKind::Objdump) + } + + #[allow(dead_code)] + pub(crate) fn readobj(&self) -> PathBuf { + self.program(ObjectToolKind::Readobj) + } + + #[allow(dead_code)] + pub(crate) fn nm(&self) -> PathBuf { + self.program(ObjectToolKind::Nm) + } } #[cfg(test)] diff --git a/ostool/src/artifact/state.rs b/ostool/src/artifact/state.rs index 517b4cda..b4be956f 100644 --- a/ostool/src/artifact/state.rs +++ b/ostool/src/artifact/state.rs @@ -12,7 +12,6 @@ use crate::artifact::runtime::PreparedRuntimeArtifacts; pub struct OutputArtifacts { cargo: Option, runtime: RuntimeArtifactState, - #[allow(dead_code)] debug: DebugArtifactRegistry, } @@ -31,20 +30,17 @@ struct RuntimeArtifactState { source_artifact_dir: Option, } -#[allow(dead_code)] #[derive(Default, Clone, Debug)] pub(crate) struct DebugArtifactRegistry { artifacts: BTreeMap, } -#[allow(dead_code)] #[derive(Clone, Debug, PartialEq, Eq)] pub(crate) struct DebugArtifact { kind: DebugArtifactKind, path: PathBuf, } -#[allow(dead_code)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] pub(crate) enum DebugArtifactKind { Disassembly, @@ -52,8 +48,8 @@ pub(crate) enum DebugArtifactKind { Symbols, } -#[allow(dead_code)] impl DebugArtifactRegistry { + #[allow(dead_code)] pub(crate) fn is_empty(&self) -> bool { self.artifacts.is_empty() } @@ -62,6 +58,7 @@ impl DebugArtifactRegistry { self.artifacts.insert(kind, DebugArtifact { kind, path }); } + #[allow(dead_code)] pub(crate) fn get(&self, kind: DebugArtifactKind) -> Option<&Path> { self.artifacts .get(&kind) @@ -135,7 +132,6 @@ impl OutputArtifacts { &self.debug } - #[cfg(test)] pub(crate) fn register_debug_artifact(&mut self, kind: DebugArtifactKind, path: PathBuf) { self.debug.register(kind, path); } diff --git a/ostool/src/board/config.rs b/ostool/src/board/config.rs index 6e32594b..cdae517b 100644 --- a/ostool/src/board/config.rs +++ b/ostool/src/board/config.rs @@ -414,6 +414,7 @@ dtb_file = "${package}/board.dtb" pre_build_cmds: vec![], post_build_cmds: vec![], to_bin: false, + artifacts: Default::default(), }), }, None, diff --git a/ostool/src/boot/fit.rs b/ostool/src/boot/fit.rs index 19f0c5f5..e73e8051 100644 --- a/ostool/src/boot/fit.rs +++ b/ostool/src/boot/fit.rs @@ -33,6 +33,7 @@ pub(crate) struct FitInput { pub(crate) kernel_load_addr: u64, pub(crate) kernel_entry_addr: u64, pub(crate) fdt_load_addr: Option, + pub(crate) kernel_os: Option, pub(crate) output_path: Option, } @@ -83,6 +84,7 @@ pub(crate) async fn generate_fit_image(input: FitInput) -> anyhow::Result, + kernel_os: &str, ) -> FitImageConfig { let mut config = FitImageConfig::new(FIT_DESCRIPTION).with_kernel( ComponentConfig::new(KERNEL_COMPONENT_NAME, kernel_data) .with_description("This kernel") .with_type("kernel") .with_arch(arch_name) - .with_os("linux") + .with_os(kernel_os) .with_compression(false) .with_load_address(kernel_load_addr) .with_entry_point(kernel_entry_addr), @@ -200,7 +203,15 @@ mod tests { #[test] fn default_fit_config_keeps_linux_kernel_defaults_without_dtb() { - let config = build_default_fit_config("arm64", vec![1, 2, 3], None, 0x80000, 0x80000, None); + let config = build_default_fit_config( + "arm64", + vec![1, 2, 3], + None, + 0x80000, + 0x80000, + None, + "linux", + ); let kernel = config.kernel.as_ref().unwrap(); assert_eq!( @@ -231,6 +242,7 @@ mod tests { 0x8020_0000, 0x8020_0000, Some(0x8800_0000), + "u-boot", ); let fdt = config.fdt.as_ref().unwrap(); let default_config = config.configurations.get("config-ostool").unwrap(); @@ -239,6 +251,10 @@ mod tests { assert_eq!(fdt.component_type.as_deref(), Some("flat_dt")); assert_eq!(fdt.arch.as_deref(), Some("riscv")); assert_eq!(fdt.load_address, Some(0x8800_0000)); + assert_eq!( + config.kernel.as_ref().unwrap().os.as_deref(), + Some("u-boot") + ); assert_eq!(default_config.fdt.as_deref(), Some("fdt")); } @@ -257,6 +273,7 @@ mod tests { kernel_load_addr: 0x80000, kernel_entry_addr: 0x80000, fdt_load_addr: None, + kernel_os: None, output_path: None, }) .await @@ -286,6 +303,7 @@ mod tests { kernel_load_addr: 0x8020_0000, kernel_entry_addr: 0x8020_0000, fdt_load_addr: Some(0x8800_0000), + kernel_os: Some("linux".into()), output_path: Some(output_path.clone()), }) .await diff --git a/ostool/src/boot/mod.rs b/ostool/src/boot/mod.rs index 39c6c823..2af3edb9 100644 --- a/ostool/src/boot/mod.rs +++ b/ostool/src/boot/mod.rs @@ -2,3 +2,4 @@ pub(crate) mod artifacts; pub(crate) mod fit; +pub mod prepare; diff --git a/ostool/src/boot/prepare.rs b/ostool/src/boot/prepare.rs new file mode 100644 index 00000000..ce3062ff --- /dev/null +++ b/ostool/src/boot/prepare.rs @@ -0,0 +1,447 @@ +//! Prepare boot artifacts from the canonical runtime ELF. + +use std::path::{Path, PathBuf}; + +use anyhow::Context as _; +use chrono::Utc; +use object::Architecture; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use tokio::fs; + +use crate::{ + artifact::elf_metadata::read_elf_boot_metadata, + boot::fit::{self, FitInput}, + build::config::BuildConfig, + invocation::Invocation, + utils::PathResultExt, +}; + +pub use crate::artifact::elf_metadata::{ElfBootMetadata, ElfLoadSegment}; + +const MANIFEST_VERSION: u8 = 1; +const MANIFEST_FILE_NAME: &str = "boot-artifacts.json"; +const DEFAULT_FIT_FILE_NAME: &str = "image.fit"; +const BOOT_SCRIPT_FILE_NAME: &str = "boot.cmd"; +const ROOTFS_DIR_NAME: &str = "rootfs"; +const BOOT_PARTITION_DIR_NAME: &str = "boot-partition"; + +/// Options for `ostool boot prepare`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BootPrepareOptions { + /// Output directory for the prepared boot package. + pub output_dir: Option, + /// Optional DTB path to copy into the package and attach to FIT. + pub dtb_path: Option, + /// FIT image generation options. + pub fit: FitPrepareOptions, + /// Create the rootfs staging directory. + pub rootfs: bool, + /// Create the boot partition staging directory. + pub boot_partition: bool, +} + +impl Default for BootPrepareOptions { + fn default() -> Self { + Self { + output_dir: None, + dtb_path: None, + fit: FitPrepareOptions::default(), + rootfs: true, + boot_partition: true, + } + } +} + +/// FIT image generation options for boot preparation. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct FitPrepareOptions { + /// Whether to generate a FIT image. + pub enabled: bool, + /// Optional override for the FIT kernel `load` property. + pub kernel_load_addr: Option, + /// Optional override for the FIT kernel `entry` property. + pub kernel_entry_addr: Option, + /// Optional override for the FIT FDT `load` property. + pub fdt_load_addr: Option, + /// FIT kernel `os` property. Defaults to `linux` for compatibility. + pub kernel_os: String, +} + +impl Default for FitPrepareOptions { + fn default() -> Self { + Self { + enabled: true, + kernel_load_addr: None, + kernel_entry_addr: None, + fdt_load_addr: None, + kernel_os: "linux".into(), + } + } +} + +/// Stable v1 manifest for prepared boot artifacts. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct PreparedBootArtifacts { + pub version: u8, + pub generated_at: String, + pub kernel_elf: PathBuf, + #[serde(skip_serializing_if = "Option::is_none")] + pub kernel_bin: Option, + pub elf_metadata: ElfBootMetadata, + pub artifacts: Vec, + #[serde(skip)] + pub manifest_path: PathBuf, +} + +impl PreparedBootArtifacts { + pub fn manifest_path(&self) -> &Path { + &self.manifest_path + } +} + +/// One file or directory in a prepared boot package. +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct PreparedBootArtifact { + pub kind: PreparedBootArtifactKind, + pub path: PathBuf, +} + +/// Prepared boot package artifact kind. +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum PreparedBootArtifactKind { + Dtb, + FitImage, + BootScript, + RootfsDir, + BootPartitionDir, +} + +/// Builds runtime artifacts, then prepares the boot artifact package. +pub async fn build_and_prepare_boot_artifacts( + invocation: &mut Invocation, + config: &BuildConfig, + config_path: Option<&Path>, + options: BootPrepareOptions, +) -> anyhow::Result { + crate::build::prepare_runtime_artifacts(invocation, config, config_path, false).await?; + prepare_boot_artifacts(invocation, options).await +} + +/// Prepares a boot artifact package from an already prepared runtime ELF. +pub async fn prepare_boot_artifacts( + invocation: &mut Invocation, + options: BootPrepareOptions, +) -> anyhow::Result { + let output_dir = options + .output_dir + .clone() + .unwrap_or_else(|| invocation.build_dir().join("boot")); + fs::create_dir_all(&output_dir) + .await + .with_path("failed to create directory", &output_dir)?; + + let kernel_elf = invocation + .runtime_artifacts() + .elf() + .ok_or_else(|| anyhow!("boot prepare requires a prepared ELF artifact"))? + .to_path_buf(); + let elf_metadata = read_elf_boot_metadata(&kernel_elf)?; + let dtb_path = prepare_dtb(&output_dir, options.dtb_path.as_deref()).await?; + let mut artifacts = Vec::new(); + + if let Some(path) = &dtb_path { + artifacts.push(PreparedBootArtifact { + kind: PreparedBootArtifactKind::Dtb, + path: path.clone(), + }); + } + + let mut kernel_bin = invocation.runtime_artifacts().bin().map(PathBuf::from); + let fit_path = if options.fit.enabled { + kernel_bin = Some(invocation.ensure_runtime_bin()?); + let generated = prepare_fit_image( + invocation, + &options.fit, + &elf_metadata, + dtb_path.clone(), + &output_dir, + ) + .await?; + let path = generated.path().to_path_buf(); + artifacts.push(PreparedBootArtifact { + kind: PreparedBootArtifactKind::FitImage, + path: path.clone(), + }); + Some(path) + } else { + None + }; + + let boot_script = prepare_boot_script(&output_dir).await?; + artifacts.push(PreparedBootArtifact { + kind: PreparedBootArtifactKind::BootScript, + path: boot_script.clone(), + }); + + if options.rootfs { + let rootfs = output_dir.join(ROOTFS_DIR_NAME); + fs::create_dir_all(&rootfs) + .await + .with_path("failed to create directory", &rootfs)?; + artifacts.push(PreparedBootArtifact { + kind: PreparedBootArtifactKind::RootfsDir, + path: rootfs, + }); + } + + if options.boot_partition { + let boot_partition = prepare_boot_partition( + &output_dir, + fit_path.as_deref(), + dtb_path.as_deref(), + &boot_script, + ) + .await?; + artifacts.push(PreparedBootArtifact { + kind: PreparedBootArtifactKind::BootPartitionDir, + path: boot_partition, + }); + } + + let manifest_path = output_dir.join(MANIFEST_FILE_NAME); + let manifest = PreparedBootArtifacts { + version: MANIFEST_VERSION, + generated_at: Utc::now().to_rfc3339(), + kernel_elf, + kernel_bin, + elf_metadata, + artifacts, + manifest_path, + }; + write_manifest(&manifest).await?; + Ok(manifest) +} + +async fn prepare_fit_image( + invocation: &mut Invocation, + options: &FitPrepareOptions, + metadata: &ElfBootMetadata, + dtb_path: Option, + output_dir: &Path, +) -> anyhow::Result { + let arch = invocation + .runtime_arch() + .ok_or_else(|| anyhow!("Cannot determine architecture for FIT image generation"))?; + reject_non_boot_fit_arch(arch)?; + let kernel_path = invocation.ensure_runtime_bin()?; + fit::generate_fit_image(FitInput { + kernel_path, + dtb_path, + arch, + kernel_load_addr: options.kernel_load_addr.unwrap_or(metadata.load), + kernel_entry_addr: options.kernel_entry_addr.unwrap_or(metadata.entry), + fdt_load_addr: options.fdt_load_addr, + kernel_os: Some(options.kernel_os.clone()), + output_path: Some(output_dir.join(DEFAULT_FIT_FILE_NAME)), + }) + .await +} + +fn reject_non_boot_fit_arch(arch: Architecture) -> anyhow::Result<()> { + fit::fit_arch_name(arch).map(|_| ()) +} + +async fn prepare_dtb( + output_dir: &Path, + dtb_path: Option<&Path>, +) -> anyhow::Result> { + let Some(dtb_path) = dtb_path else { + return Ok(None); + }; + let file_name = dtb_path + .file_name() + .ok_or_else(|| anyhow!("invalid DTB file path: {}", dtb_path.display()))?; + let output_path = output_dir.join(file_name); + fs::copy(dtb_path, &output_path).await.with_context(|| { + format!( + "failed to copy DTB from {} to {}", + dtb_path.display(), + output_path.display() + ) + })?; + Ok(Some(output_path)) +} + +async fn prepare_boot_script(output_dir: &Path) -> anyhow::Result { + let output_path = output_dir.join(BOOT_SCRIPT_FILE_NAME); + fs::write( + &output_path, + "echo Loading ostool FIT image\nload mmc 0:1 ${loadaddr} /image.fit\nbootm ${loadaddr}\n", + ) + .await + .with_path("failed to write file", &output_path)?; + Ok(output_path) +} + +async fn prepare_boot_partition( + output_dir: &Path, + fit_path: Option<&Path>, + dtb_path: Option<&Path>, + boot_script: &Path, +) -> anyhow::Result { + let boot_partition = output_dir.join(BOOT_PARTITION_DIR_NAME); + fs::create_dir_all(&boot_partition) + .await + .with_path("failed to create directory", &boot_partition)?; + + if let Some(path) = fit_path { + copy_to_dir(path, &boot_partition, Path::new(DEFAULT_FIT_FILE_NAME)).await?; + } + if let Some(path) = dtb_path { + let file_name = path + .file_name() + .ok_or_else(|| anyhow!("invalid DTB file path: {}", path.display()))?; + copy_to_dir(path, &boot_partition, Path::new(file_name)).await?; + } + copy_to_dir( + boot_script, + &boot_partition, + Path::new(BOOT_SCRIPT_FILE_NAME), + ) + .await?; + + Ok(boot_partition) +} + +async fn copy_to_dir( + source: &Path, + dest_dir: &Path, + file_name: impl AsRef, +) -> anyhow::Result { + let dest = dest_dir.join(file_name); + fs::copy(source, &dest).await.with_context(|| { + format!( + "failed to copy boot artifact from {} to {}", + source.display(), + dest.display() + ) + })?; + Ok(dest) +} + +async fn write_manifest(manifest: &PreparedBootArtifacts) -> anyhow::Result<()> { + let content = serde_json::to_vec_pretty(manifest)?; + fs::write(&manifest.manifest_path, content) + .await + .with_path("failed to write file", &manifest.manifest_path) +} + +#[cfg(test)] +mod tests { + use std::fs; + + use crate::{ + artifact::{ + runtime::{RuntimeArtifactOptions, prepare_runtime_artifacts}, + state::OutputArtifacts, + }, + boot::prepare::{ + BootPrepareOptions, FitPrepareOptions, PreparedBootArtifactKind, prepare_boot_artifacts, + }, + invocation::{Invocation, InvocationOptions}, + }; + + fn write_single_crate_manifest(dir: &std::path::Path) { + fs::write( + dir.join("Cargo.toml"), + "[package]\nname = \"sample\"\nversion = \"0.1.0\"\nedition = \"2024\"\n", + ) + .unwrap(); + fs::create_dir_all(dir.join("src")).unwrap(); + fs::write(dir.join("src/lib.rs"), "").unwrap(); + } + + #[tokio::test] + async fn prepare_boot_artifacts_writes_v1_manifest_without_fit() { + let temp = tempfile::tempdir().unwrap(); + write_single_crate_manifest(temp.path()); + let source = std::env::current_exe().unwrap(); + let input = temp.path().join("sample-elf"); + fs::copy(source, &input).unwrap(); + let mut invocation = Invocation::new(InvocationOptions::new( + Some(temp.path().to_path_buf()), + None, + None, + false, + )) + .unwrap(); + let prepared = prepare_runtime_artifacts( + &invocation.process_context().unwrap(), + RuntimeArtifactOptions { + elf_path: input, + to_bin: false, + bin_dir: None, + debug: false, + cargo_artifact_dir: None, + strip_elf: false, + objcopy_program: "false".into(), + }, + ) + .unwrap(); + invocation.apply_prepared_runtime_artifacts(prepared); + let output_dir = temp.path().join("target").join("boot"); + let dtb = temp.path().join("board.dtb"); + fs::write(&dtb, [1_u8, 2, 3]).unwrap(); + + let manifest = prepare_boot_artifacts( + &mut invocation, + BootPrepareOptions { + output_dir: Some(output_dir.clone()), + dtb_path: Some(dtb), + fit: FitPrepareOptions { + enabled: false, + ..FitPrepareOptions::default() + }, + rootfs: true, + boot_partition: true, + }, + ) + .await + .unwrap(); + + assert_eq!(manifest.version, 1); + assert_eq!( + manifest.manifest_path(), + output_dir.join("boot-artifacts.json") + ); + assert!(manifest.kernel_bin.is_none()); + assert!( + manifest + .artifacts + .iter() + .any(|artifact| artifact.kind == PreparedBootArtifactKind::Dtb) + ); + assert!( + manifest + .artifacts + .iter() + .any(|artifact| artifact.kind == PreparedBootArtifactKind::BootScript) + ); + assert!(output_dir.join("rootfs").is_dir()); + assert!(output_dir.join("boot-partition").join("boot.cmd").is_file()); + + let manifest_json = fs::read_to_string(output_dir.join("boot-artifacts.json")).unwrap(); + assert!(manifest_json.contains("\"version\": 1")); + assert!(manifest_json.contains("\"elf_metadata\"")); + } + + #[test] + fn output_artifacts_default_remains_empty() { + let artifacts = OutputArtifacts::default(); + + assert!(artifacts.elf().is_none()); + assert!(artifacts.bin().is_none()); + } +} diff --git a/ostool/src/build/config.rs b/ostool/src/build/config.rs index 2bc8d358..f938ce43 100644 --- a/ostool/src/build/config.rs +++ b/ostool/src/build/config.rs @@ -16,6 +16,11 @@ //! # Optional legacy override. Runners that require BIN artifacts prepare them //! # automatically. //! to_bin = false +//! +//! [system.Cargo.artifacts] +//! disassembly = false +//! elf_info = false +//! symbols = false //! ``` use std::collections::HashMap; @@ -23,6 +28,8 @@ use std::collections::HashMap; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +pub use crate::artifact::analysis::AnalysisArtifactConfig; + fn is_false(value: &bool) -> bool { !*value } @@ -69,6 +76,9 @@ pub struct Custom { /// Runners that require BIN artifacts prepare them automatically. #[serde(default)] pub to_bin: bool, + /// Optional analysis artifacts derived from the prepared ELF. + #[serde(default)] + pub artifacts: AnalysisArtifactConfig, } /// Configuration for Cargo-based builds. @@ -121,6 +131,9 @@ pub struct Cargo { /// Runners that require BIN artifacts prepare them automatically. #[serde(default)] pub to_bin: bool, + /// Optional analysis artifacts derived from the prepared ELF. + #[serde(default)] + pub artifacts: AnalysisArtifactConfig, } /// Cargo build profile selection. @@ -180,6 +193,7 @@ to_bin = false .unwrap(); assert!(!cargo.disable_someboot_build_config); + assert!(cargo.artifacts.is_empty()); } #[test] @@ -198,6 +212,7 @@ post_build_cmds = [] .unwrap(); assert!(!cargo.to_bin); + assert!(cargo.artifacts.is_empty()); } #[test] @@ -211,6 +226,7 @@ elf_path = "target/kernel" .unwrap(); assert!(!custom.to_bin); + assert!(custom.artifacts.is_empty()); } #[test] diff --git a/ostool/src/build/mod.rs b/ostool/src/build/mod.rs index 551156c2..5420ccc7 100644 --- a/ostool/src/build/mod.rs +++ b/ostool/src/build/mod.rs @@ -22,9 +22,12 @@ use std::path::{Path, PathBuf}; use anyhow::bail; use crate::{ - artifact::object_tools::ObjectTools, - artifact::runtime::{ - RuntimeArtifactOptions, prepare_runtime_artifacts as prepare_runtime_artifact_outputs, + artifact::{ + analysis::{AnalysisArtifactConfig, generate_analysis_artifacts}, + object_tools::ObjectTools, + runtime::{ + RuntimeArtifactOptions, prepare_runtime_artifacts as prepare_runtime_artifact_outputs, + }, }, build::{ cargo_pipeline::{CargoBuildInput, CargoBuildOutcome, CargoBuildPipeline}, @@ -272,7 +275,7 @@ pub async fn cargo_build( } /// Builds or imports the configured artifact and prepares the runtime outputs. -pub(crate) async fn prepare_runtime_artifacts( +pub async fn prepare_runtime_artifacts( invocation: &mut Invocation, config: &config::BuildConfig, config_path: Option<&Path>, @@ -296,7 +299,8 @@ async fn prepare_custom_runtime_artifacts( build_custom(invocation, config)?; invocation .prepare_elf_artifact(config.elf_path.clone().into(), config.to_bin) - .await + .await?; + prepare_configured_analysis_artifacts(invocation, &config.artifacts) } async fn prepare_cargo_runtime_artifacts( @@ -410,6 +414,40 @@ fn apply_cargo_build_outcome( }, )?; invocation.apply_prepared_runtime_artifacts(prepared); + prepare_configured_analysis_artifacts(invocation, &config.artifacts)?; + Ok(()) +} + +fn prepare_configured_analysis_artifacts( + invocation: &mut Invocation, + config: &AnalysisArtifactConfig, +) -> anyhow::Result<()> { + if config.is_empty() { + return Ok(()); + } + + let elf_path = invocation + .runtime_artifacts() + .elf() + .ok_or_else(|| anyhow!("elf not exist"))? + .to_path_buf(); + let output_dir = invocation + .runtime_artifacts() + .runtime_artifact_dir() + .or_else(|| elf_path.parent()) + .ok_or_else(|| anyhow!("invalid ELF file path: {}", elf_path.display()))? + .to_path_buf(); + let process_context = invocation.process_context()?; + let mut artifacts = invocation.runtime_artifacts().clone(); + generate_analysis_artifacts( + &process_context, + &elf_path, + &output_dir, + config, + &ObjectTools, + &mut artifacts, + )?; + invocation.replace_runtime_artifacts(artifacts); Ok(()) } @@ -526,6 +564,7 @@ mod tests { build_cmd: format!("printf built > {}", marker.display()), elf_path: "target/kernel.elf".into(), to_bin: true, + artifacts: Default::default(), }), }; @@ -625,6 +664,7 @@ mod tests { build_cmd: "make".into(), elf_path: "target/kernel.elf".into(), to_bin: true, + artifacts: Default::default(), }), }; diff --git a/ostool/src/invocation.rs b/ostool/src/invocation.rs index 409b51e3..3d8d8ec6 100644 --- a/ostool/src/invocation.rs +++ b/ostool/src/invocation.rs @@ -165,6 +165,10 @@ impl Invocation { self.state.apply_prepared_runtime_artifacts(&prepared); } + pub(crate) fn replace_runtime_artifacts(&mut self, artifacts: OutputArtifacts) { + self.state.replace_runtime_artifacts(artifacts); + } + pub(crate) fn ensure_runtime_bin(&mut self) -> anyhow::Result { self.ensure_runtime_bin_with_objcopy(ObjectTools.objcopy()) } @@ -273,6 +277,10 @@ impl InvocationState { self.artifacts.apply_prepared_runtime_artifacts(prepared); self.arch = prepared.arch(); } + + pub(crate) fn replace_runtime_artifacts(&mut self, artifacts: OutputArtifacts) { + self.artifacts = artifacts; + } } /// Build configuration after CLI overrides and package scope resolution. diff --git a/ostool/src/lib.rs b/ostool/src/lib.rs index 59176e95..c6fd02fb 100644 --- a/ostool/src/lib.rs +++ b/ostool/src/lib.rs @@ -33,7 +33,7 @@ #![cfg(not(target_os = "none"))] mod artifact; -mod boot; +pub mod boot; /// Build system configuration and Cargo integration. /// diff --git a/ostool/src/main.rs b/ostool/src/main.rs index 01a0f027..6aed6ad1 100644 --- a/ostool/src/main.rs +++ b/ostool/src/main.rs @@ -10,6 +10,7 @@ use env_logger::Env; use log::info; use ostool::{ board, + boot::prepare::{BootPrepareOptions, FitPrepareOptions, PreparedBootArtifacts}, build::{self, CargoQemuRunnerArgs, CargoRunnerKind, CargoUbootRunnerArgs}, invocation::{Invocation, InvocationOptions}, menuconfig::{MenuConfigHandler, MenuConfigMode}, @@ -42,6 +43,7 @@ enum SubCommands { command: RunSubCommands, }, Board(BoardArgs), + Boot(BootArgs), Menuconfig { /// Menu configuration mode (qemu or uboot) #[arg(value_enum)] @@ -65,6 +67,12 @@ struct BoardArgs { command: BoardSubCommands, } +#[derive(Args, Debug)] +struct BootArgs { + #[command(subcommand)] + command: BootSubCommands, +} + #[derive(Subcommand, Debug)] enum BoardSubCommands { Ls(BoardServerArgs), @@ -73,6 +81,65 @@ enum BoardSubCommands { Config, } +#[derive(Subcommand, Debug)] +enum BootSubCommands { + Prepare(BootPrepareCommand), +} + +#[derive(Args, Debug)] +struct BootPrepareCommand { + /// Path to the build configuration file + #[arg(short, long)] + config: Option, + #[command(flatten)] + cargo_selector: CargoSelectorArgs, + /// Output directory for prepared boot artifacts. Defaults to target/boot. + #[arg(long)] + output_dir: Option, + /// Optional DTB copied into the boot package and attached to FIT. + #[arg(long)] + dtb: Option, + /// Disable FIT generation. + #[arg(long)] + no_fit: bool, + /// Disable rootfs staging directory generation. + #[arg(long)] + no_rootfs: bool, + /// Disable boot partition staging directory generation. + #[arg(long)] + no_boot_partition: bool, + /// Override the FIT kernel os property. + #[arg(long, default_value = "linux")] + fit_kernel_os: String, + /// Override the FIT kernel load address. Accepts decimal or 0x-prefixed hex. + #[arg(long, value_parser = parse_u64_arg)] + kernel_load_addr: Option, + /// Override the FIT kernel entry address. Accepts decimal or 0x-prefixed hex. + #[arg(long, value_parser = parse_u64_arg)] + kernel_entry_addr: Option, + /// Override the FIT FDT load address. Accepts decimal or 0x-prefixed hex. + #[arg(long, value_parser = parse_u64_arg)] + fdt_load_addr: Option, +} + +impl BootPrepareCommand { + fn options(&self) -> BootPrepareOptions { + BootPrepareOptions { + output_dir: self.output_dir.clone(), + dtb_path: self.dtb.clone(), + fit: FitPrepareOptions { + enabled: !self.no_fit, + kernel_load_addr: self.kernel_load_addr, + kernel_entry_addr: self.kernel_entry_addr, + fdt_load_addr: self.fdt_load_addr, + kernel_os: self.fit_kernel_os.clone(), + }, + rootfs: !self.no_rootfs, + boot_partition: !self.no_boot_partition, + } + } +} + #[derive(Args, Debug)] struct RunQemuCommand { /// Path to the build configuration file @@ -228,6 +295,27 @@ async fn try_main() -> Result<()> { board::config()?; } }, + SubCommands::Boot(args) => match args.command { + BootSubCommands::Prepare(args) => { + let mut invocation = init_invocation(manifest)?; + let mut loaded_build_config = + load_build_config(&invocation, args.config.as_deref()).await?; + apply_cargo_selector( + &mut invocation, + &mut loaded_build_config.config, + loaded_build_config.path.as_path(), + &args.cargo_selector, + )?; + let prepared = ostool::boot::prepare::build_and_prepare_boot_artifacts( + &mut invocation, + &loaded_build_config.config, + Some(loaded_build_config.path.as_path()), + args.options(), + ) + .await?; + print_boot_prepare_summary(&prepared); + } + }, SubCommands::Build { config, cargo_selector, @@ -292,19 +380,14 @@ async fn try_main() -> Result<()> { ) .await?; } - build::config::BuildSystem::Custom(custom_cfg) => { - build::build_with_config( + build::config::BuildSystem::Custom(_) => { + build::prepare_runtime_artifacts( &mut invocation, &loaded_build_config.config, Some(loaded_build_config.path.as_path()), + debug, ) .await?; - invocation - .prepare_elf_artifact( - custom_cfg.elf_path.clone().into(), - custom_cfg.to_bin, - ) - .await?; let qemu_config = load_qemu_config(&mut invocation, qemu.qemu_config.as_deref()).await?; ostool::run::qemu::run_qemu( @@ -356,19 +439,14 @@ async fn try_main() -> Result<()> { ) .await?; } - build::config::BuildSystem::Custom(custom_cfg) => { - build::build_with_config( + build::config::BuildSystem::Custom(_) => { + build::prepare_runtime_artifacts( &mut invocation, &loaded_build_config.config, Some(loaded_build_config.path.as_path()), + false, ) .await?; - invocation - .prepare_elf_artifact( - custom_cfg.elf_path.clone().into(), - custom_cfg.to_bin, - ) - .await?; let uboot_config = load_uboot_config(&mut invocation, uboot.uboot_config.as_deref()) .await?; @@ -398,6 +476,33 @@ fn init_invocation(manifest_arg: Option) -> Result { Ok(invocation) } +fn parse_u64_arg(value: &str) -> std::result::Result { + if let Some(hex) = value + .strip_prefix("0x") + .or_else(|| value.strip_prefix("0X")) + { + return u64::from_str_radix(hex, 16) + .map_err(|err| format!("invalid hexadecimal address `{value}`: {err}")); + } + value + .parse::() + .map_err(|err| format!("invalid address `{value}`: {err}")) +} + +fn print_boot_prepare_summary(prepared: &PreparedBootArtifacts) { + println!( + "{}", + format!( + "Prepared boot artifacts:\n manifest: {}\n kernel elf: {}\n artifacts: {}", + prepared.manifest_path().display(), + prepared.kernel_elf.display(), + prepared.artifacts.len() + ) + .bold() + .green() + ); +} + struct LoadedBuildConfig { config: build::config::BuildConfig, path: PathBuf, @@ -498,9 +603,9 @@ mod tests { use ostool::invocation::{Invocation, InvocationOptions}; use super::{ - BoardArgs, BoardSubCommands, CargoSelectorArgs, Cli, RunSubCommands, SubCommands, - apply_cargo_selector, build, load_board_config, load_build_config, load_qemu_config, - load_uboot_config, + BoardArgs, BoardSubCommands, BootSubCommands, CargoSelectorArgs, Cli, RunSubCommands, + SubCommands, apply_cargo_selector, build, load_board_config, load_build_config, + load_qemu_config, load_uboot_config, parse_u64_arg, }; /// Verifies build parsing accepts manifest, config, package, and bin overrides. @@ -618,6 +723,64 @@ mod tests { } } + #[test] + fn parse_boot_prepare_with_artifact_options() { + let cli = Cli::try_parse_from([ + "ostool", + "boot", + "prepare", + "--config", + "kernel.build.toml", + "--package", + "kernel", + "--bin", + "kernel-boot", + "--output-dir", + "target/boot", + "--dtb", + "virt.dtb", + "--no-fit", + "--no-rootfs", + "--kernel-load-addr", + "0x80200000", + "--kernel-entry-addr", + "0x80200000", + "--fdt-load-addr", + "2147483648", + ]) + .unwrap(); + + match cli.command { + SubCommands::Boot(args) => match args.command { + BootSubCommands::Prepare(args) => { + assert_eq!( + args.config.as_deref(), + Some(std::path::Path::new("kernel.build.toml")) + ); + assert_eq!(args.cargo_selector.package.as_deref(), Some("kernel")); + assert_eq!(args.cargo_selector.bin.as_deref(), Some("kernel-boot")); + assert_eq!( + args.output_dir.as_deref(), + Some(std::path::Path::new("target/boot")) + ); + assert_eq!(args.dtb.as_deref(), Some(std::path::Path::new("virt.dtb"))); + assert!(args.no_fit); + assert!(args.no_rootfs); + assert_eq!(args.kernel_load_addr, Some(0x8020_0000)); + assert_eq!(args.kernel_entry_addr, Some(0x8020_0000)); + assert_eq!(args.fdt_load_addr, Some(2_147_483_648)); + } + }, + other => panic!("unexpected command: {other:?}"), + } + } + + #[test] + fn parse_u64_arg_accepts_decimal_and_hex() { + assert_eq!(parse_u64_arg("42").unwrap(), 42); + assert_eq!(parse_u64_arg("0x2a").unwrap(), 42); + } + #[test] fn parse_board_ls_with_server_args() { let cli = Cli::try_parse_from([ @@ -810,6 +973,7 @@ mod tests { build_cmd: "make".into(), elf_path: "target/kernel.elf".into(), to_bin: true, + artifacts: Default::default(), }), }; diff --git a/ostool/src/run/qemu.rs b/ostool/src/run/qemu.rs index 48533dcd..455beddf 100644 --- a/ostool/src/run/qemu.rs +++ b/ostool/src/run/qemu.rs @@ -20,6 +20,9 @@ //! to_bin = true //! success_regex = ["All tests passed"] //! fail_regex = ["PANIC", "FAILED"] +//! +//! [boot] +//! mode = "direct" //! ``` use std::{ @@ -97,6 +100,46 @@ pub struct QemuConfig { pub shell_init_cmd: Option, /// Timeout in seconds. `None` or `0` disables the timeout. pub timeout: Option, + /// Boot mode selection. `uboot` is recognized but requires explicit firmware. + #[serde(default, skip_serializing_if = "QemuBootConfig::is_default")] + pub boot: QemuBootConfig, +} + +/// QEMU boot-mode configuration. +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +pub struct QemuBootConfig { + /// Boot mode for QEMU. + #[serde(default)] + pub mode: QemuBootMode, + /// Explicit firmware path for boot modes that need firmware. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub firmware: Option, +} + +impl Default for QemuBootConfig { + fn default() -> Self { + Self { + mode: QemuBootMode::Direct, + firmware: None, + } + } +} + +impl QemuBootConfig { + fn is_default(&self) -> bool { + self == &Self::default() + } +} + +/// QEMU boot mode. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub enum QemuBootMode { + /// Load the prepared runtime image directly with QEMU. + #[default] + Direct, + /// Boot through U-Boot firmware. + Uboot, } impl QemuConfig { @@ -126,6 +169,12 @@ impl QemuConfig { .as_deref() .map(|value| variables::expand_variables(value, scope)) .transpose()?; + self.boot.firmware = self + .boot + .firmware + .as_deref() + .map(|value| variables::expand_variables(value, scope)) + .transpose()?; Ok(()) } @@ -144,6 +193,18 @@ impl QemuConfig { fn requires_bin_artifact(&self) -> bool { self.uefi || self.to_bin } + + fn validate_boot_mode(&self) -> anyhow::Result<()> { + match self.boot.mode { + QemuBootMode::Direct => Ok(()), + QemuBootMode::Uboot if self.boot.firmware.is_none() => anyhow::bail!( + "QEMU U-Boot boot requires `boot.firmware`; firmware preparation is not implemented yet" + ), + QemuBootMode::Uboot => anyhow::bail!( + "QEMU U-Boot boot with explicit firmware is recognized but not wired into QEMU execution yet" + ), + } + } } /// Pure execution options for running an already prepared artifact in QEMU. @@ -288,6 +349,7 @@ pub(crate) async fn run_qemu_with_config( run_args: RunQemuOptions, config: QemuConfig, ) -> anyhow::Result<()> { + config.validate_boot_mode()?; if config.requires_bin_artifact() { input .artifacts @@ -792,10 +854,11 @@ where #[cfg(test)] mod tests { use super::{ - QemuConfig, QemuRunInput, QemuRunner, RunQemuOptions, build_default_qemu_config, - default_qemu_config_for_cargo, ensure_config_for_cargo, ensure_qemu_config_at_path, - infer_target_arch, read_config_from_path, read_qemu_config_at_path, - resolve_qemu_config_path_in_dir, run_qemu_with_config, timeout_duration, + QemuBootConfig, QemuBootMode, QemuConfig, QemuRunInput, QemuRunner, RunQemuOptions, + build_default_qemu_config, default_qemu_config_for_cargo, ensure_config_for_cargo, + ensure_qemu_config_at_path, infer_target_arch, read_config_from_path, + read_qemu_config_at_path, resolve_qemu_config_path_in_dir, run_qemu_with_config, + timeout_duration, }; use object::Architecture; use std::{ @@ -997,6 +1060,7 @@ fail_regex = [] pre_build_cmds: vec![], post_build_cmds: vec![], to_bin: false, + artifacts: Default::default(), }, ) .await @@ -1078,6 +1142,52 @@ fail_regex = [] ); } + #[test] + fn qemu_config_parses_uboot_boot_mode() { + let config: QemuConfig = toml::from_str( + r#" +args = ["-nographic"] +uefi = false +success_regex = [] +fail_regex = [] + +[boot] +mode = "uboot" +firmware = "${workspace}/target/firmware/u-boot.bin" +"#, + ) + .unwrap(); + + assert_eq!(config.boot.mode, QemuBootMode::Uboot); + assert_eq!( + config.boot.firmware.as_deref(), + Some("${workspace}/target/firmware/u-boot.bin") + ); + } + + #[tokio::test] + async fn qemu_uboot_mode_rejects_missing_firmware_before_execution() { + let tmp = TempDir::new().unwrap(); + write_single_crate_manifest(tmp.path()); + let invocation = make_invocation(tmp.path()); + + let err = run_qemu_with_config( + qemu_input(&invocation), + RunQemuOptions::default(), + QemuConfig { + boot: QemuBootConfig { + mode: QemuBootMode::Uboot, + firmware: None, + }, + ..Default::default() + }, + ) + .await + .unwrap_err(); + + assert!(err.to_string().contains("boot.firmware")); + } + #[test] fn default_qemu_config_for_cargo_uses_target_arch() { let config = default_qemu_config_for_cargo( @@ -1095,6 +1205,7 @@ fail_regex = [] pre_build_cmds: vec![], post_build_cmds: vec![], to_bin: false, + artifacts: Default::default(), }, None, ); @@ -1303,6 +1414,7 @@ fail_regex = [] pre_build_cmds: vec![], post_build_cmds: vec![], to_bin: false, + artifacts: Default::default(), }), }, None, @@ -1318,6 +1430,10 @@ fail_regex = [] fail_regex: vec!["${workspaceFolder}".into()], shell_prefix: Some("${workspace}".into()), shell_init_cmd: Some("${package}".into()), + boot: QemuBootConfig { + mode: QemuBootMode::Direct, + firmware: Some("${package}/firmware.bin".into()), + }, ..Default::default() }; @@ -1331,6 +1447,11 @@ fail_regex = [] assert_eq!(config.fail_regex, vec![expected.clone()]); assert_eq!(config.shell_prefix.as_deref(), Some(expected.as_str())); assert_eq!(config.shell_init_cmd.as_deref(), Some(expected.as_str())); + let expected_firmware = tmp.path().join("firmware.bin").display().to_string(); + assert_eq!( + config.boot.firmware.as_deref(), + Some(expected_firmware.as_str()) + ); } #[tokio::test] diff --git a/ostool/src/run/uboot.rs b/ostool/src/run/uboot.rs index 6ac87537..b15ac1f5 100644 --- a/ostool/src/run/uboot.rs +++ b/ostool/src/run/uboot.rs @@ -1177,6 +1177,7 @@ where kernel_load_addr: kernel_entry, kernel_entry_addr: kernel_entry, fdt_load_addr, + kernel_os: None, output_path: None, }) .await?; @@ -1883,6 +1884,7 @@ timeout = 0 pre_build_cmds: vec![], post_build_cmds: vec![], to_bin: false, + artifacts: Default::default(), }), }, None, @@ -2072,6 +2074,7 @@ baud_rate = "115200" pre_build_cmds: vec![], post_build_cmds: vec![], to_bin: false, + artifacts: Default::default(), }), }, None, diff --git a/ostool/tests/ui/pass_module_level_apis.rs b/ostool/tests/ui/pass_module_level_apis.rs index 3206432e..88184680 100644 --- a/ostool/tests/ui/pass_module_level_apis.rs +++ b/ostool/tests/ui/pass_module_level_apis.rs @@ -29,6 +29,7 @@ fn main() { build_cmd: "true".into(), elf_path: "target/kernel.elf".into(), to_bin: false, + artifacts: Default::default(), }), }; let qemu_config: QemuConfig = qemu::default_config_for_cargo(&invocation, &cargo);