use std::path::{Path, PathBuf};
use anyhow::{Context, bail};
use crate::{
artifact::runtime::{
RuntimeArtifactOptions, prepare_runtime_artifacts as prepare_runtime_artifact_outputs,
},
build::{
cargo_pipeline::{CargoBuildInput, CargoBuildOutcome, CargoBuildPipeline},
config::{BuildConfig, BuildSystem, Cargo, Custom},
},
invocation::{ActiveBuildContext, ActiveCargoBuild, ActiveCustomBuild, Invocation},
project::{ProjectLayout, metadata, variables::VariableScope},
run::{
qemu::{QemuConfig, RunQemuOptions},
uboot::UbootConfig,
},
};
mod artifact_selector;
pub(crate) mod config_hooks;
pub(crate) mod config_loader;
mod cargo_pipeline;
pub mod config;
pub mod someboot;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CargoBuildOutput {
elf_path: PathBuf,
cargo_artifact_dir: PathBuf,
}
impl CargoBuildOutput {
fn new(elf_path: PathBuf, cargo_artifact_dir: PathBuf) -> Self {
Self {
elf_path,
cargo_artifact_dir,
}
}
pub fn elf_path(&self) -> &Path {
&self.elf_path
}
pub fn cargo_artifact_dir(&self) -> &Path {
&self.cargo_artifact_dir
}
}
impl From<&CargoBuildOutcome> for CargoBuildOutput {
fn from(outcome: &CargoBuildOutcome) -> Self {
let resolved = outcome.resolved_artifact();
Self::new(
resolved.elf_path().to_path_buf(),
resolved.cargo_artifact_dir().to_path_buf(),
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RuntimeArtifactInput {
elf_path: PathBuf,
to_bin: bool,
cargo_artifact_dir: Option<PathBuf>,
strip_elf: bool,
}
impl RuntimeArtifactInput {
pub fn new(elf_path: impl Into<PathBuf>, to_bin: bool) -> Self {
Self {
elf_path: elf_path.into(),
to_bin,
cargo_artifact_dir: None,
strip_elf: false,
}
}
pub fn with_cargo_artifact_dir(mut self, cargo_artifact_dir: impl Into<PathBuf>) -> Self {
self.cargo_artifact_dir = Some(cargo_artifact_dir.into());
self
}
pub fn strip_elf(mut self, strip_elf: bool) -> Self {
self.strip_elf = strip_elf;
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CargoQemuRunnerArgs {
pub qemu: Option<QemuConfig>,
pub debug: bool,
pub dtb_dump: bool,
}
#[derive(Debug, Clone, Default)]
pub struct CargoUbootRunnerArgs {
pub uboot: Option<UbootConfig>,
}
pub enum CargoRunnerKind {
Qemu(Box<CargoQemuRunnerArgs>),
Uboot(Box<CargoUbootRunnerArgs>),
}
impl CargoRunnerKind {
pub fn new_qemu(args: CargoQemuRunnerArgs) -> Self {
Self::Qemu(Box::new(args))
}
pub fn new_uboot(args: CargoUbootRunnerArgs) -> Self {
Self::Uboot(Box::new(args))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct CargoSelector {
package: Option<String>,
bin: Option<String>,
test: Option<String>,
}
impl CargoSelector {
#[cfg(test)]
pub(crate) fn new(package: Option<String>, bin: Option<String>, test: Option<String>) -> Self {
Self { package, bin, test }
}
fn is_empty(&self) -> bool {
self.package.is_none() && self.bin.is_none() && self.test.is_none()
}
}
fn apply_cargo_selector(
config: &mut config::BuildConfig,
selector: &CargoSelector,
) -> anyhow::Result<()> {
if selector.is_empty() {
return Ok(());
}
let config::BuildSystem::Cargo(cargo_config) = &mut config.system else {
bail!("--package/--bin/--test can only be used with system.Cargo build configs");
};
if let Some(package) = &selector.package {
cargo_config.package = package.clone();
}
if let Some(bin) = &selector.bin {
cargo_config.bin = Some(bin.clone());
cargo_config.test = None;
}
if let Some(test) = &selector.test {
cargo_config.bin = None;
cargo_config.test = Some(test.clone());
}
Ok(())
}
pub(crate) fn activate_build_context(
layout: &ProjectLayout,
mut config: BuildConfig,
config_path: Option<PathBuf>,
selector: &CargoSelector,
) -> anyhow::Result<ActiveBuildContext> {
apply_cargo_selector(&mut config, selector)?;
match config.system {
BuildSystem::Cargo(cargo) => {
let variable_scope = cargo_variable_scope(layout, &cargo)?;
Ok(ActiveBuildContext::Cargo(Box::new(ActiveCargoBuild::new(
config_path,
variable_scope,
))))
}
BuildSystem::Custom(_) => {
let variable_scope =
VariableScope::for_package(layout, layout.manifest_dir().to_path_buf());
Ok(ActiveBuildContext::Custom(ActiveCustomBuild::new(
config_path,
variable_scope,
)))
}
}
}
pub(crate) fn cargo_variable_scope(
layout: &ProjectLayout,
cargo: &Cargo,
) -> anyhow::Result<VariableScope> {
let package_dir = metadata::package_manifest_dir(layout, &cargo.package)?;
Ok(VariableScope::for_package(layout, package_dir))
}
pub fn default_build_config() -> config::BuildConfig {
config::BuildConfig::default()
}
pub async fn load_build_config_from_dir(
invocation: &Invocation,
dir: &Path,
menu: bool,
) -> anyhow::Result<config::BuildConfig> {
prepare_build_config(invocation, Some(dir.join(".build.toml")), menu).await
}
pub async fn load_build_config_from_path(
invocation: &Invocation,
path: &Path,
menu: bool,
) -> anyhow::Result<config::BuildConfig> {
prepare_build_config(invocation, Some(path.to_path_buf()), menu).await
}
pub fn activate_build_config(
invocation: &mut Invocation,
config: &config::BuildConfig,
config_path: Option<&Path>,
) -> anyhow::Result<()> {
let active = activate_build_context(
invocation.project_layout(),
config.clone(),
config_path.map(Path::to_path_buf),
&CargoSelector::default(),
)?;
invocation.set_active_build(active);
Ok(())
}
async fn prepare_build_config(
invocation: &Invocation,
config_path: Option<PathBuf>,
menu: bool,
) -> anyhow::Result<BuildConfig> {
let hooks = config_hooks::build_config_hooks(invocation.workspace_dir());
config_loader::load_build_config(invocation.workspace_dir(), config_path, menu, &hooks).await
}
pub async fn build_with_config(
invocation: &mut Invocation,
config: &config::BuildConfig,
config_path: Option<&Path>,
) -> anyhow::Result<()> {
invocation.clear_debug_artifacts();
activate_build_config(invocation, config, config_path)?;
match &config.system {
config::BuildSystem::Custom(custom) => build_custom(invocation, custom)?,
config::BuildSystem::Cargo(cargo) => {
cargo_build(invocation, cargo, config_path).await?;
}
}
generate_build_analysis(invocation, config)
}
pub(crate) fn build_custom(invocation: &mut Invocation, config: &Custom) -> anyhow::Result<()> {
let process_context = invocation.process_context()?;
crate::process::shell_run_cmd(&process_context, &config.build_cmd)?;
Ok(())
}
pub fn prepare_runtime_artifact(
invocation: &mut Invocation,
input: RuntimeArtifactInput,
) -> anyhow::Result<()> {
invocation.clear_debug_artifacts();
let process_context = invocation.process_context()?;
let prepared = prepare_runtime_artifact_outputs(
&process_context,
RuntimeArtifactOptions {
elf_path: input.elf_path,
to_bin: input.to_bin,
bin_dir: invocation.bin_dir(),
debug: invocation.options().debug(),
cargo_artifact_dir: input.cargo_artifact_dir,
strip_elf: input.strip_elf,
},
)?;
invocation.apply_prepared_runtime_artifacts(prepared);
Ok(())
}
pub async fn cargo_build(
invocation: &mut Invocation,
config: &Cargo,
config_path: Option<&Path>,
) -> anyhow::Result<CargoBuildOutput> {
invocation.clear_debug_artifacts();
activate_build_config(
invocation,
&BuildConfig {
artifacts: Default::default(),
system: BuildSystem::Cargo(Box::new(config.clone())),
},
config_path,
)?;
let debug = invocation.options().debug();
let input = cargo_build_input(invocation, config, debug)?;
let outcome = CargoBuildPipeline::build(input, config).execute().await?;
apply_cargo_build_outcome(invocation, config, &outcome, false, debug)?;
run_cargo_post_build_cmds(invocation, config)?;
Ok(CargoBuildOutput::from(&outcome))
}
pub async fn prepare_with_config(
invocation: &mut Invocation,
config: &config::BuildConfig,
config_path: Option<&Path>,
debug: bool,
) -> anyhow::Result<()> {
invocation.clear_debug_artifacts();
activate_build_config(invocation, config, config_path)?;
match &config.system {
config::BuildSystem::Custom(custom) => {
prepare_custom_runtime_artifacts(invocation, custom).await?;
}
config::BuildSystem::Cargo(cargo) => {
prepare_cargo_runtime_artifacts(invocation, cargo, debug).await?;
}
}
generate_build_analysis(invocation, config)
}
fn generate_build_analysis(
invocation: &mut Invocation,
config: &BuildConfig,
) -> anyhow::Result<()> {
if !config.artifacts.analysis.is_enabled() {
return Ok(());
}
let source = match &config.system {
BuildSystem::Cargo(_) => invocation
.runtime_artifacts()
.cargo_source_elf()
.context("Cargo build did not select an ELF for analysis")?
.to_path_buf(),
BuildSystem::Custom(custom) => {
let path = PathBuf::from(&custom.elf_path);
if path.is_absolute() {
path
} else {
invocation.manifest_dir().join(path)
}
}
};
let artifacts = crate::artifact::analysis::generate(
&invocation.process_context()?,
&config.artifacts.analysis,
&source,
)?;
invocation.replace_debug_artifacts(artifacts);
Ok(())
}
async fn prepare_custom_runtime_artifacts(
invocation: &mut Invocation,
config: &Custom,
) -> anyhow::Result<()> {
build_custom(invocation, config)?;
let path = PathBuf::from(&config.elf_path);
let path = if path.is_absolute() {
path
} else {
invocation.manifest_dir().join(path)
};
invocation.prepare_elf_artifact(path, config.to_bin).await
}
async fn prepare_cargo_runtime_artifacts(
invocation: &mut Invocation,
config: &Cargo,
debug: bool,
) -> anyhow::Result<()> {
let input = cargo_build_input(invocation, config, debug)?;
let outcome = CargoBuildPipeline::build(input, config)
.skip_objcopy(true)
.resolve_artifact_from_json(true)
.execute()
.await?;
apply_cargo_build_outcome(invocation, config, &outcome, true, debug)?;
run_cargo_post_build_cmds(invocation, config)?;
Ok(())
}
pub async fn cargo_run(
invocation: &mut Invocation,
config: &Cargo,
config_path: Option<&Path>,
runner: &CargoRunnerKind,
) -> anyhow::Result<()> {
cargo_run_with_config(
invocation,
&BuildConfig {
system: BuildSystem::Cargo(Box::new(config.clone())),
artifacts: Default::default(),
},
config_path,
runner,
)
.await
}
pub async fn cargo_run_with_config(
invocation: &mut Invocation,
config: &BuildConfig,
config_path: Option<&Path>,
runner: &CargoRunnerKind,
) -> anyhow::Result<()> {
let BuildSystem::Cargo(cargo) = &config.system else {
anyhow::bail!("cargo_run_with_config requires a Cargo build configuration");
};
let debug = matches!(runner, CargoRunnerKind::Qemu(args) if args.debug);
prepare_with_config(invocation, config, config_path, debug).await?;
match runner {
CargoRunnerKind::Qemu(args) => {
let qemu = match &args.qemu {
Some(config) => config.clone(),
None => crate::run::qemu::ensure_config_for_cargo(invocation, cargo).await?,
};
crate::run::qemu::run_qemu_with_debug(
invocation,
&qemu,
RunQemuOptions {
dtb_dump: args.dtb_dump,
},
debug,
)
.await?;
}
CargoRunnerKind::Uboot(args) => {
let uboot = match &args.uboot {
Some(config) => config.clone(),
None => crate::run::uboot::ensure_config_for_cargo(invocation, cargo).await?,
};
crate::run::uboot::run_uboot(invocation, &uboot).await?;
}
}
Ok(())
}
fn cargo_build_input(
invocation: &Invocation,
config: &Cargo,
debug: bool,
) -> anyhow::Result<CargoBuildInput> {
Ok(CargoBuildInput::new(
invocation.project_layout().clone(),
invocation.process_context()?,
invocation.build_dir(),
invocation
.state()
.build_config_path()
.map(std::path::Path::to_path_buf),
debug,
!config.disable_someboot_build_config,
))
}
fn apply_cargo_build_outcome(
invocation: &mut Invocation,
config: &Cargo,
outcome: &CargoBuildOutcome,
skip_objcopy: bool,
debug: bool,
) -> anyhow::Result<()> {
let resolved = outcome.resolved_artifact();
let process_context = invocation.process_context()?;
let prepared = prepare_runtime_artifact_outputs(
&process_context,
RuntimeArtifactOptions {
elf_path: resolved.elf_path().to_path_buf(),
to_bin: config.to_bin && !skip_objcopy,
bin_dir: invocation.bin_dir(),
debug,
cargo_artifact_dir: Some(resolved.cargo_artifact_dir().to_path_buf()),
strip_elf: false,
},
)?;
invocation.apply_prepared_runtime_artifacts(prepared);
Ok(())
}
fn run_cargo_post_build_cmds(invocation: &mut Invocation, config: &Cargo) -> anyhow::Result<()> {
let process_context = invocation.process_context()?;
for cmd in &config.post_build_cmds {
crate::process::shell_run_cmd(&process_context, cmd)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use std::{fs, path::PathBuf};
use crate::{
build::{
artifact_selector::ResolvedCargoArtifact,
cargo_pipeline::CargoBuildOutcome,
config::{BuildConfig, BuildSystem, Cargo, CargoBuildProfile, Custom},
},
invocation::{Invocation, InvocationOptions},
project::resolve_project_layout,
};
use super::{
CargoBuildOutput, CargoSelector, RuntimeArtifactInput, activate_build_config,
activate_build_context, apply_cargo_build_outcome, build_with_config,
prepare_runtime_artifact,
};
#[test]
fn apply_cargo_build_outcome_records_runtime_artifact_state() {
let temp = tempfile::tempdir().unwrap();
fs::write(
temp.path().join("Cargo.toml"),
"[package]\nname = \"kernel\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
let cargo_artifact_dir = temp.path().join("target/aarch64/debug");
fs::create_dir_all(&cargo_artifact_dir).unwrap();
let elf_path = cargo_artifact_dir.join("kernel");
fs::copy(std::env::current_exe().unwrap(), &elf_path).unwrap();
let config = Cargo {
target: "aarch64-unknown-none".into(),
package: "kernel".into(),
profile: Some(CargoBuildProfile::Debug),
to_bin: true,
..Default::default()
};
let mut invocation = Invocation::new(InvocationOptions::new(
Some(temp.path().to_path_buf()),
None,
None,
false,
))
.unwrap();
let outcome = CargoBuildOutcome::new(ResolvedCargoArtifact::new(
elf_path.clone(),
cargo_artifact_dir.clone(),
));
apply_cargo_build_outcome(&mut invocation, &config, &outcome, true, false).unwrap();
let expected_elf = elf_path.canonicalize().unwrap();
assert_eq!(
invocation.runtime_artifacts().elf(),
Some(expected_elf.as_path())
);
assert!(invocation.runtime_artifacts().bin().is_none());
assert_eq!(
invocation.runtime_artifacts().cargo_artifact_dir(),
Some(cargo_artifact_dir.as_path())
);
assert_eq!(
invocation.runtime_artifacts().cargo_source_artifact_dir(),
Some(cargo_artifact_dir.as_path())
);
assert_eq!(
invocation.runtime_artifacts().cargo_source_elf(),
Some(expected_elf.as_path())
);
assert_eq!(
invocation.runtime_artifacts().runtime_artifact_dir(),
Some(cargo_artifact_dir.as_path())
);
assert!(invocation.runtime_artifacts().debug_artifacts().is_empty());
assert!(invocation.runtime_arch().is_some());
}
#[test]
fn cargo_build_output_exposes_resolved_artifact_paths() {
let cargo_artifact_dir = PathBuf::from("target/aarch64/release");
let elf_path = cargo_artifact_dir.join("kernel");
let outcome = CargoBuildOutcome::new(ResolvedCargoArtifact::new(
elf_path.clone(),
cargo_artifact_dir.clone(),
));
let output = CargoBuildOutput::from(&outcome);
assert_eq!(output.elf_path(), elf_path.as_path());
assert_eq!(output.cargo_artifact_dir(), cargo_artifact_dir.as_path());
}
#[test]
fn prepare_runtime_artifact_records_external_artifact_state() {
let temp = tempfile::tempdir().unwrap();
fs::write(
temp.path().join("Cargo.toml"),
"[package]\nname = \"kernel\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
let cargo_artifact_dir = temp.path().join("target/aarch64/debug");
fs::create_dir_all(&cargo_artifact_dir).unwrap();
let elf_path = cargo_artifact_dir.join("kernel");
fs::copy(std::env::current_exe().unwrap(), &elf_path).unwrap();
let mut invocation = Invocation::new(InvocationOptions::new(
Some(temp.path().to_path_buf()),
None,
None,
false,
))
.unwrap();
prepare_runtime_artifact(
&mut invocation,
RuntimeArtifactInput::new(&elf_path, false)
.with_cargo_artifact_dir(cargo_artifact_dir.clone())
.strip_elf(true),
)
.unwrap();
let expected_elf = elf_path.canonicalize().unwrap();
assert_eq!(
invocation.runtime_artifacts().elf(),
Some(elf_path.with_extension("elf").as_path())
);
assert!(invocation.runtime_artifacts().bin().is_none());
assert_eq!(
invocation.runtime_artifacts().cargo_artifact_dir(),
Some(cargo_artifact_dir.as_path())
);
assert_eq!(
invocation.runtime_artifacts().cargo_source_artifact_dir(),
Some(cargo_artifact_dir.as_path())
);
assert_eq!(
invocation.runtime_artifacts().cargo_source_elf(),
Some(expected_elf.as_path())
);
assert!(invocation.runtime_arch().is_some());
}
#[tokio::test]
async fn custom_build_only_does_not_prepare_runtime_artifacts() {
let temp = tempfile::tempdir().unwrap();
fs::write(
temp.path().join("Cargo.toml"),
"[package]\nname = \"kernel\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
let marker = temp.path().join("custom-build-ran");
let mut invocation = Invocation::new(InvocationOptions::new(
Some(temp.path().to_path_buf()),
None,
None,
false,
))
.unwrap();
let mut stale = crate::artifact::state::DebugArtifactRegistry::default();
stale.register(
crate::artifact::state::DebugArtifactKind::Symbols,
temp.path().join("old.symbols"),
);
invocation.replace_debug_artifacts(stale);
let config = BuildConfig {
artifacts: Default::default(),
system: BuildSystem::Custom(Custom {
build_cmd: format!("printf built > {}", marker.display()),
elf_path: "target/kernel.elf".into(),
to_bin: true,
}),
};
build_with_config(&mut invocation, &config, None)
.await
.unwrap();
assert_eq!(fs::read_to_string(marker).unwrap(), "built");
assert!(invocation.runtime_artifacts().elf().is_none());
assert!(invocation.runtime_artifacts().bin().is_none());
assert!(invocation.runtime_arch().is_none());
assert!(invocation.runtime_artifacts().debug_artifacts().is_empty());
}
#[test]
fn activate_build_context_applies_cargo_selector_and_scope() {
let temp = tempfile::tempdir().unwrap();
fs::write(
temp.path().join("Cargo.toml"),
"[package]\nname = \"kernel\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
fs::create_dir_all(temp.path().join("src/bin")).unwrap();
fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
fs::write(temp.path().join("src/bin/kernel-qemu.rs"), "fn main() {}\n").unwrap();
let layout = resolve_project_layout(Some(temp.path().to_path_buf())).unwrap();
let config_path = temp.path().join(".build.toml");
let config = BuildConfig {
artifacts: Default::default(),
system: BuildSystem::Cargo(Box::new(Cargo {
package: "placeholder".into(),
..Default::default()
})),
};
let active = activate_build_context(
&layout,
config,
Some(config_path.clone()),
&CargoSelector::new(Some("kernel".into()), Some("kernel-qemu".into()), None),
)
.unwrap();
let crate::invocation::ActiveBuildContext::Cargo(active) = active else {
panic!("expected active Cargo build");
};
assert_eq!(active.config_path(), Some(config_path.as_path()));
assert_eq!(active.variable_scope().package_dir(), temp.path());
}
#[test]
fn activate_build_config_uses_explicit_path_and_clears_absent_path() {
let temp = tempfile::tempdir().unwrap();
fs::write(
temp.path().join("Cargo.toml"),
"[package]\nname = \"kernel\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
let mut invocation = Invocation::new(InvocationOptions::new(
Some(temp.path().to_path_buf()),
None,
None,
false,
))
.unwrap();
let config = BuildConfig {
artifacts: Default::default(),
system: BuildSystem::Cargo(Box::new(Cargo {
package: "kernel".into(),
..Default::default()
})),
};
let config_path = temp.path().join(".build.toml");
activate_build_config(&mut invocation, &config, Some(&config_path)).unwrap();
assert_eq!(
invocation.state().build_config_path(),
Some(config_path.as_path())
);
activate_build_config(&mut invocation, &config, None).unwrap();
assert!(invocation.state().build_config_path().is_none());
}
#[test]
fn activate_build_context_rejects_selector_for_custom_build() {
let temp = tempfile::tempdir().unwrap();
fs::write(
temp.path().join("Cargo.toml"),
"[package]\nname = \"kernel\"\nversion = \"0.1.0\"\nedition = \"2024\"\n",
)
.unwrap();
fs::create_dir_all(temp.path().join("src")).unwrap();
fs::write(temp.path().join("src/main.rs"), "fn main() {}\n").unwrap();
let layout = resolve_project_layout(Some(temp.path().to_path_buf())).unwrap();
let config = BuildConfig {
artifacts: Default::default(),
system: BuildSystem::Custom(Custom {
build_cmd: "make".into(),
elf_path: "target/kernel.elf".into(),
to_bin: true,
}),
};
let err = activate_build_context(
&layout,
config,
None,
&CargoSelector::new(Some("kernel".into()), None, None),
)
.unwrap_err();
assert!(
err.to_string()
.contains("--package/--bin/--test can only be used with system.Cargo")
);
}
}