use std::path::{Path, PathBuf};
use anyhow::Context;
use ostool::{
board::{RunBoardOptions, config::BoardRunConfig},
build::config::Cargo,
};
use crate::{
axvisor::context::AxvisorContext,
command_flow::{self, SnapshotPersistence},
context::{AppContext, AxvisorCliArgs, ResolvedAxvisorRequest},
test_qemu,
};
pub mod board;
pub mod build;
pub mod cli;
pub mod config;
pub mod context;
pub mod image;
pub mod qemu;
pub mod qemu_test;
pub use cli::{
ArgsBoard, ArgsBuild, ArgsConfig, ArgsDefconfig, ArgsQemu, ArgsTest, ArgsUboot, Command,
ConfigCommand, TestCommand,
};
pub struct Axvisor {
app: AppContext,
ctx: AxvisorContext,
}
impl Axvisor {
pub fn new() -> anyhow::Result<Self> {
let app = AppContext::new()?;
let ctx = AxvisorContext::new()?;
Ok(Self { app, ctx })
}
pub async fn execute(&mut self, command: Command) -> anyhow::Result<()> {
match command {
Command::Build(args) => self.build(args).await,
Command::Qemu(args) => self.qemu(args).await,
Command::Uboot(args) => self.uboot(args).await,
Command::Board(args) => self.board(args).await,
Command::Defconfig(args) => self.defconfig(args),
Command::Config(args) => self.config(args),
Command::Image(args) => self.image(args).await,
Command::Test(args) => self.test(args).await,
}
}
async fn build(&mut self, args: ArgsBuild) -> anyhow::Result<()> {
let request =
self.prepare_request((&args).into(), None, None, SnapshotPersistence::Store)?;
self.run_build_request(request).await
}
async fn qemu(&mut self, args: ArgsQemu) -> anyhow::Result<()> {
let request = self.prepare_request(
(&args.build).into(),
args.qemu_config,
None,
SnapshotPersistence::Store,
)?;
if qemu::infer_rootfs_path(&request.vmconfigs)?.is_none() {
qemu_test::prepare_default_rootfs_for_arch(&self.ctx, &request.arch).await?;
}
self.run_qemu_request(request).await
}
async fn uboot(&mut self, args: ArgsUboot) -> anyhow::Result<()> {
let request = self.prepare_request(
(&args.build).into(),
None,
args.uboot_config,
SnapshotPersistence::Store,
)?;
self.run_uboot_request(request).await
}
async fn board(&mut self, args: ArgsBoard) -> anyhow::Result<()> {
let request =
self.prepare_request((&args.build).into(), None, None, SnapshotPersistence::Store)?;
self.app.set_debug_mode(request.debug)?;
let cargo = build::load_cargo_config(&request)?;
let board_config = self
.load_board_config(&cargo, args.board_config.as_deref())
.await?;
self.app
.board(
cargo,
request.build_info_path,
board_config,
RunBoardOptions {
board_type: args.board_type,
server: args.server,
port: args.port,
},
)
.await
}
fn defconfig(&mut self, args: ArgsDefconfig) -> anyhow::Result<()> {
let workspace_root = self.app.workspace_root().to_path_buf();
let axvisor_dir = self.app.axvisor_dir()?.to_path_buf();
let path = config::write_defconfig(&workspace_root, &axvisor_dir, &args.board)?;
println!("Generated {} for board {}", path.display(), args.board);
Ok(())
}
fn config(&mut self, args: ArgsConfig) -> anyhow::Result<()> {
match args.command {
ConfigCommand::Ls => {
for board in config::available_board_names(self.app.axvisor_dir()?)? {
println!("{board}");
}
}
}
Ok(())
}
async fn image(&self, args: image::Args) -> anyhow::Result<()> {
image::run(args, &self.ctx).await
}
async fn test(&mut self, args: ArgsTest) -> anyhow::Result<()> {
match args.command {
TestCommand::Qemu(args) => self.test_qemu(args).await,
TestCommand::Uboot(args) => self.test_uboot(args).await,
TestCommand::Board(args) => self.test_board(args).await,
}
}
async fn test_qemu(&mut self, args: cli::ArgsTestQemu) -> anyhow::Result<()> {
let (arch, target) = test_qemu::parse_axvisor_test_target(&args.target)?;
println!(
"running axvisor qemu tests for arch: {} (target: {})",
arch, target
);
let vmconfig = match arch {
"aarch64" => {
qemu_test::prepare_linux_aarch64_guest_assets(&self.ctx)
.await?
.generated_vmconfig
}
"x86_64" => qemu_test::prepare_nimbos_x86_64_guest_vmconfig(&self.ctx).await?,
_ => unreachable!(),
};
let request = self.prepare_request(
qemu_test::qemu_test_build_args(arch, vmconfig),
None,
None,
SnapshotPersistence::Discard,
)?;
let shell = test_qemu::axvisor_test_shell_config(arch)?;
let cargo = build::load_cargo_config(&request)?;
let mut qemu_config = self.load_qemu_config(&request, &cargo).await?;
qemu_test::apply_shell_autoinit_config(&mut qemu_config, &shell);
self.app
.qemu(cargo, request.build_info_path, Some(qemu_config))
.await
.with_context(|| "axvisor qemu test failed")
}
async fn test_uboot(&mut self, args: cli::ArgsTestUboot) -> anyhow::Result<()> {
let board = test_qemu::axvisor_uboot_board_config(&args.board)?;
let explicit_uboot_config = args.uboot_config.clone();
let uboot_config_summary = explicit_uboot_config
.as_ref()
.map(|path| path.display().to_string())
.unwrap_or_else(|| "using ostool default search".to_string());
if let Some(path) = explicit_uboot_config.as_ref()
&& !path.exists()
{
bail!(
"missing explicit U-Boot config `{}` for axvisor board tests",
path.display()
);
}
println!(
"running axvisor uboot test for board: {} with vmconfig: {}",
board.board, board.vmconfig
);
let mut request = self.prepare_request(
qemu_test::uboot_test_build_args(board.build_config, board.vmconfig),
None,
explicit_uboot_config.clone(),
SnapshotPersistence::Discard,
)?;
request.uboot_config = explicit_uboot_config;
let cargo = build::load_cargo_config(&request)?;
let uboot = self.load_uboot_config(&request, &cargo).await?;
self.app
.uboot(cargo, request.build_info_path, uboot)
.await
.with_context(|| {
format!(
"axvisor uboot test failed for board `{}` (build_config={}, vmconfig={}, \
uboot_config={})",
board.board, board.build_config, board.vmconfig, uboot_config_summary
)
})
}
async fn test_board(&mut self, args: cli::ArgsTestBoard) -> anyhow::Result<()> {
if args.board_test_config.is_some() && args.test_group.is_none() {
bail!(
"`--board-test-config` requires `--test-group` because board test configs embed a \
single board_type"
);
}
if let Some(path) = args.board_test_config.as_ref()
&& !path.exists()
{
bail!("missing explicit board test config `{}`", path.display());
}
let groups = test_qemu::axvisor_board_test_groups(args.test_group.as_deref())?;
let total = groups.len();
let mut failed = Vec::new();
for (index, group) in groups.into_iter().enumerate() {
let board_test_config = args
.board_test_config
.clone()
.unwrap_or_else(|| PathBuf::from(group.board_test_config));
let board_test_config_summary = board_test_config.display().to_string();
if !board_test_config.exists() {
eprintln!(
"failed: {}: missing board test config `{}`",
group.name, board_test_config_summary
);
failed.push(group.name.to_string());
continue;
}
println!("[{}/{}] axvisor board {}", index + 1, total, group.name);
let result = async {
let request = self.prepare_request(
qemu_test::board_test_build_args(&group),
None,
None,
SnapshotPersistence::Discard,
)?;
let cargo = build::load_cargo_config(&request)?;
let board_config = self
.load_board_config(&cargo, Some(board_test_config.as_path()))
.await?;
self.app
.board(
cargo,
request.build_info_path,
board_config,
RunBoardOptions {
board_type: args.board_type.clone(),
server: args.server.clone(),
port: args.port,
},
)
.await
.with_context(|| {
format!(
"axvisor board test failed for group `{}` (build_config={}, \
board_test_config={}, vmconfigs={})",
group.name,
group.build_config,
board_test_config_summary,
group.vmconfigs.join(", ")
)
})
}
.await;
match result {
Ok(()) => println!("ok: {}", group.name),
Err(err) => {
eprintln!("failed: {}: {:#}", group.name, err);
failed.push(group.name.to_string());
}
}
}
test_qemu::finalize_board_test_run(&failed)
}
fn prepare_request(
&mut self,
args: AxvisorCliArgs,
qemu_config: Option<PathBuf>,
uboot_config: Option<PathBuf>,
persistence: SnapshotPersistence,
) -> anyhow::Result<ResolvedAxvisorRequest> {
let (request, snapshot) =
self.app
.prepare_axvisor_request(args, qemu_config, uboot_config)?;
if matches!(persistence, SnapshotPersistence::Store) {
self.app.store_axvisor_snapshot(&snapshot)?;
}
Ok(request)
}
async fn load_qemu_config(
&mut self,
request: &ResolvedAxvisorRequest,
cargo: &Cargo,
) -> anyhow::Result<ostool::run::qemu::QemuConfig> {
let config_path = request.qemu_config.clone().unwrap_or_else(|| {
qemu::default_qemu_config_template_path(&request.axvisor_dir, &request.arch)
});
let mut qemu = self
.app
.tool_mut()
.read_qemu_config_from_path_for_cargo(cargo, &config_path)
.await?;
qemu::apply_rootfs_path(&mut qemu, request)?;
Ok(qemu)
}
async fn load_uboot_config(
&mut self,
request: &ResolvedAxvisorRequest,
cargo: &Cargo,
) -> anyhow::Result<Option<ostool::run::uboot::UbootConfig>> {
match request.uboot_config.as_deref() {
Some(path) => self
.app
.tool_mut()
.read_uboot_config_from_path_for_cargo(cargo, path)
.await
.map(Some),
None => Ok(None),
}
}
async fn load_board_config(
&mut self,
cargo: &Cargo,
board_config_path: Option<&Path>,
) -> anyhow::Result<BoardRunConfig> {
match board_config_path {
Some(path) => {
self.app
.tool_mut()
.read_board_run_config_from_path_for_cargo(cargo, path)
.await
}
None => {
let workspace_root = self.app.workspace_root().to_path_buf();
self.app
.tool_mut()
.ensure_board_run_config_in_dir_for_cargo(cargo, &workspace_root)
.await
}
}
}
async fn run_qemu_request(&mut self, request: ResolvedAxvisorRequest) -> anyhow::Result<()> {
self.app.set_debug_mode(request.debug)?;
let cargo = build::load_cargo_config(&request)?;
let qemu = self.load_qemu_config(&request, &cargo).await?;
self.app
.qemu(cargo, request.build_info_path, Some(qemu))
.await
}
async fn run_build_request(&mut self, request: ResolvedAxvisorRequest) -> anyhow::Result<()> {
command_flow::run_build(&mut self.app, request, build::load_cargo_config).await
}
async fn run_uboot_request(&mut self, request: ResolvedAxvisorRequest) -> anyhow::Result<()> {
self.app.set_debug_mode(request.debug)?;
let cargo = build::load_cargo_config(&request)?;
let uboot = self.load_uboot_config(&request, &cargo).await?;
self.app.uboot(cargo, request.build_info_path, uboot).await
}
}
impl Default for Axvisor {
fn default() -> Self {
Self::new().expect("failed to initialize Axvisor")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::context::{workspace_member_dir, workspace_root_path};
#[test]
fn context_resolves_workspace_root() {
let ctx = AxvisorContext::new().unwrap();
assert_eq!(
ctx.workspace_root(),
workspace_root_path().unwrap().as_path()
);
assert_eq!(
ctx.axvisor_dir(),
workspace_member_dir(crate::axvisor::build::AXVISOR_PACKAGE)
.unwrap()
.as_path()
);
}
#[test]
fn default_qemu_template_path_uses_axvisor_script_location() {
let path = qemu::default_qemu_config_template_path(Path::new("os/axvisor"), "aarch64");
assert_eq!(
path,
PathBuf::from("os/axvisor/scripts/ostool/qemu-aarch64.toml")
);
}
}