use std::{
collections::BTreeSet,
path::{Path, PathBuf},
process::Command as StdCommand,
};
use anyhow::Context;
use clap::{Args, Subcommand};
use ostool::{build::config::Cargo, run::qemu::QemuConfig};
use crate::context::{AppContext, BuildCliArgs, ResolvedBuildRequest, SnapshotPersistence};
const DEFAULT_TEST_DISK_IMAGE_SIZE: &str = "64M";
pub(super) fn ensure_qemu_runtime_assets(
workspace_root: &Path,
qemu: &QemuConfig,
) -> anyhow::Result<()> {
let mut seen = BTreeSet::new();
for image in qemu_runtime_disk_images(qemu) {
if !seen.insert(image.clone()) {
continue;
}
ensure_fat32_image(
&image,
DEFAULT_TEST_DISK_IMAGE_SIZE,
should_recreate_runtime_image(workspace_root, &image),
)?;
}
Ok(())
}
fn ensure_fat32_image(image: &Path, size: &str, recreate: bool) -> anyhow::Result<()> {
if image.exists() && !recreate {
return Ok(());
}
let msg = format!("generating {}", image.display());
println!("{msg} ...");
if let Some(parent) = image.parent() {
std::fs::create_dir_all(parent)?;
}
if image.exists() {
std::fs::remove_file(image)?;
}
let ran = |cmd: &mut StdCommand| -> anyhow::Result<()> {
let name = cmd.get_program().to_string_lossy().to_string();
cmd.status()
.with_context(|| format!("failed to run `{name}`"))?
.success()
.then_some(())
.ok_or_else(|| anyhow::anyhow!("`{name}` exited with non-zero status"))
};
ran(StdCommand::new("truncate").args(["-s", size]).arg(image))?;
ran(StdCommand::new("mkfs.fat")
.args(["-F", "32"])
.arg(image)
.stdout(std::process::Stdio::null()))?;
println!("{msg} ... done");
Ok(())
}
fn qemu_runtime_disk_images(qemu: &QemuConfig) -> Vec<PathBuf> {
crate::rootfs::qemu::drive_file_paths(qemu)
.into_iter()
.filter(|path| path.file_name().and_then(|name| name.to_str()) == Some("disk.img"))
.collect()
}
fn should_recreate_runtime_image(workspace_root: &Path, image: &Path) -> bool {
image.starts_with(crate::context::axbuild_tmp_dir(workspace_root).join("runtime-assets"))
}
pub mod build;
pub mod cbuild;
pub mod rootfs;
pub mod test;
#[derive(Subcommand)]
pub enum Command {
Build(ArgsBuild),
Qemu(ArgsQemu),
Test(test::ArgsTest),
Uboot(ArgsUboot),
}
#[derive(Args)]
pub struct ArgsBuild {
#[arg(short, long)]
pub config: Option<PathBuf>,
#[arg(short, long)]
pub package: Option<String>,
#[arg(long)]
pub arch: Option<String>,
#[arg(short, long)]
pub target: Option<String>,
#[arg(long = "plat_dyn", alias = "plat-dyn")]
pub plat_dyn: Option<bool>,
#[arg(long, value_name = "CPUS")]
pub smp: Option<usize>,
#[arg(long)]
pub debug: bool,
}
#[derive(Args)]
pub struct ArgsQemu {
#[command(flatten)]
pub build: ArgsBuild,
#[arg(long)]
pub qemu_config: Option<PathBuf>,
#[arg(long, value_name = "IMAGE")]
pub rootfs: Option<PathBuf>,
}
#[derive(Args)]
pub struct ArgsUboot {
#[command(flatten)]
pub build: ArgsBuild,
#[arg(long)]
pub uboot_config: Option<PathBuf>,
}
pub struct ArceOS {
pub(super) app: AppContext,
}
impl From<&ArgsBuild> for BuildCliArgs {
fn from(args: &ArgsBuild) -> Self {
Self {
config: args.config.clone(),
package: args.package.clone(),
arch: args.arch.clone(),
target: args.target.clone(),
plat_dyn: args.plat_dyn,
smp: args.smp,
debug: args.debug,
}
}
}
impl ArceOS {
pub fn new() -> anyhow::Result<Self> {
let app = AppContext::new()?;
Ok(Self { app })
}
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::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 let Some(rootfs) = args.rootfs {
rootfs::qemu_with_explicit_rootfs(self, request, rootfs).await
} else {
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 test(&mut self, args: test::ArgsTest) -> anyhow::Result<()> {
test::test(self, args).await
}
pub(super) fn prepare_request(
&self,
args: BuildCliArgs,
qemu_config: Option<PathBuf>,
uboot_config: Option<PathBuf>,
persistence: SnapshotPersistence,
) -> anyhow::Result<ResolvedBuildRequest> {
let (request, snapshot) = self.app.prepare_arceos_request(
args,
qemu_config,
uboot_config,
build::resolve_build_info_path,
)?;
if persistence.should_store() {
self.app.store_arceos_snapshot(&snapshot)?;
}
Ok(request)
}
pub(super) async fn load_qemu_config(
&mut self,
request: &ResolvedBuildRequest,
cargo: &Cargo,
) -> anyhow::Result<Option<ostool::run::qemu::QemuConfig>> {
match request.qemu_config.as_deref() {
Some(path) => self
.app
.tool_mut()
.read_qemu_config_from_path_for_cargo(cargo, path)
.await
.map(Some),
None => Ok(None),
}
}
async fn load_uboot_config(
&mut self,
request: &ResolvedBuildRequest,
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 run_qemu_request(&mut self, request: ResolvedBuildRequest) -> anyhow::Result<()> {
let cargo = build::load_cargo_config(&request)?;
self.run_qemu_request_with_cargo(request, cargo).await
}
async fn run_qemu_request_with_cargo(
&mut self,
request: ResolvedBuildRequest,
cargo: Cargo,
) -> anyhow::Result<()> {
self.app.set_debug_mode(request.debug)?;
let qemu = self.load_qemu_config(&request, &cargo).await?;
if let Some(qemu) = qemu.as_ref() {
ensure_qemu_runtime_assets(self.app.workspace_root(), qemu)?;
}
self.app.qemu(cargo, request.build_info_path, qemu).await
}
async fn run_build_request(&mut self, request: ResolvedBuildRequest) -> anyhow::Result<()> {
self.app.set_debug_mode(request.debug)?;
let cargo = build::load_cargo_config(&request)?;
self.app.build(cargo, request.build_info_path).await
}
async fn run_uboot_request(&mut self, request: ResolvedBuildRequest) -> 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
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn qemu_runtime_disk_images_finds_disk_img_drive_paths() {
let qemu = QemuConfig {
args: vec![
"-drive".to_string(),
"id=disk0,if=none,format=raw,file=/tmp/case/disk.img".to_string(),
"-drive".to_string(),
"id=rootfs,if=none,format=raw,file=/tmp/rootfs.img".to_string(),
],
..Default::default()
};
assert_eq!(
qemu_runtime_disk_images(&qemu),
vec![PathBuf::from("/tmp/case/disk.img")]
);
}
#[test]
fn should_recreate_only_tmp_runtime_asset_images() {
let workspace = Path::new("/workspace");
assert!(should_recreate_runtime_image(
workspace,
Path::new("/workspace/tmp/axbuild/runtime-assets/arceos/std/case/disk.img")
));
assert!(!should_recreate_runtime_image(
workspace,
Path::new("/workspace/test-suit/arceos/rust/fs/shell/disk.img")
));
}
}