use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::{Context, ensure};
use super::{case_build, test_suit::StarryQemuCase};
const CASE_WORK_ROOT_NAME: &str = "starry-cases";
const CASE_STAGING_DIR_NAME: &str = "staging-root";
const CASE_BUILD_DIR_NAME: &str = "build";
const CASE_OVERLAY_DIR_NAME: &str = "overlay";
const CASE_COMMAND_WRAPPER_DIR_NAME: &str = "guest-bin";
const CASE_CROSS_BIN_DIR_NAME: &str = "cross-bin";
const CASE_CMAKE_TOOLCHAIN_FILE_NAME: &str = "cmake-toolchain.cmake";
const CASE_APK_CACHE_DIR_NAME: &str = "apk-cache";
const CASE_SH_DIR_NAME: &str = "sh";
const USB_STICK_IMAGE_NAME: &str = "usb-stick.raw";
const USB_STICK_IMAGE_SIZE: u64 = 16 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct StarryCaseAssets {
pub(crate) rootfs_path: PathBuf,
pub(crate) extra_qemu_args: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CaseAssetLayout {
pub(crate) work_dir: PathBuf,
pub(crate) staging_root: PathBuf,
pub(crate) build_dir: PathBuf,
pub(crate) overlay_dir: PathBuf,
pub(crate) command_wrapper_dir: PathBuf,
pub(crate) cross_bin_dir: PathBuf,
pub(crate) cmake_toolchain_file: PathBuf,
pub(crate) apk_cache_dir: PathBuf,
pub(crate) usb_stick_path: PathBuf,
}
pub(crate) fn resolve_target_dir(workspace_root: &Path, target: &str) -> anyhow::Result<PathBuf> {
let _ = crate::context::starry_arch_for_target_checked(target)?;
Ok(workspace_root.join("target").join(target))
}
pub(crate) async fn prepare_case_assets(
workspace_root: &Path,
arch: &str,
target: &str,
case: &StarryQemuCase,
rootfs_path: PathBuf,
) -> anyhow::Result<StarryCaseAssets> {
let needs_assets = case_uses_c_pipeline(case)
|| case_uses_sh_pipeline(case)
|| case_uses_usb_qemu_assets(arch, case);
if !needs_assets {
return Ok(StarryCaseAssets {
rootfs_path,
extra_qemu_args: Vec::new(),
});
}
let workspace_root = workspace_root.to_path_buf();
let arch = arch.to_string();
let target = target.to_string();
let rootfs_path_for_task = rootfs_path.clone();
let case = case.clone();
let extra_qemu_args = tokio::task::spawn_blocking(move || {
prepare_case_assets_sync(
&workspace_root,
&arch,
&target,
&case,
&rootfs_path_for_task,
)
})
.await
.context("starry case asset task failed")??;
Ok(StarryCaseAssets {
rootfs_path,
extra_qemu_args,
})
}
pub(crate) fn case_uses_c_pipeline(case: &StarryQemuCase) -> bool {
case_build::case_c_source_dir(case).is_dir()
}
pub(crate) fn case_sh_source_dir(case: &StarryQemuCase) -> PathBuf {
case.case_dir.join(CASE_SH_DIR_NAME)
}
pub(crate) fn case_uses_sh_pipeline(case: &StarryQemuCase) -> bool {
case_sh_source_dir(case).is_dir()
}
pub(crate) fn case_uses_usb_qemu_assets(arch: &str, case: &StarryQemuCase) -> bool {
let _ = arch;
let _ = case;
false
}
pub(crate) fn case_asset_layout(
workspace_root: &Path,
target: &str,
case_name: &str,
) -> anyhow::Result<CaseAssetLayout> {
let target_dir = resolve_target_dir(workspace_root, target)?;
let work_dir = target_dir.join(CASE_WORK_ROOT_NAME).join(case_name);
Ok(CaseAssetLayout {
staging_root: work_dir.join(CASE_STAGING_DIR_NAME),
build_dir: work_dir.join(CASE_BUILD_DIR_NAME),
overlay_dir: work_dir.join(CASE_OVERLAY_DIR_NAME),
command_wrapper_dir: work_dir.join(CASE_COMMAND_WRAPPER_DIR_NAME),
cross_bin_dir: work_dir.join(CASE_CROSS_BIN_DIR_NAME),
cmake_toolchain_file: work_dir.join(CASE_CMAKE_TOOLCHAIN_FILE_NAME),
apk_cache_dir: work_dir.join(CASE_APK_CACHE_DIR_NAME),
usb_stick_path: work_dir.join(USB_STICK_IMAGE_NAME),
work_dir,
})
}
pub(crate) fn prepare_case_assets_sync(
workspace_root: &Path,
arch: &str,
target: &str,
case: &StarryQemuCase,
case_rootfs: &Path,
) -> anyhow::Result<Vec<String>> {
let layout = case_asset_layout(workspace_root, target, &case.name)?;
fs::create_dir_all(&layout.work_dir)
.with_context(|| format!("failed to create {}", layout.work_dir.display()))?;
if case_uses_c_pipeline(case) {
case_build::prepare_c_case_assets_sync(arch, case, case_rootfs, &layout)?;
} else if case_uses_sh_pipeline(case) {
prepare_sh_case_assets_sync(case, case_rootfs, &layout)?;
}
let mut extra_qemu_args = Vec::new();
if case_uses_usb_qemu_assets(arch, case) {
create_usb_backing_image(&layout.usb_stick_path)?;
extra_qemu_args.extend(usb_qemu_args(&layout.usb_stick_path));
}
Ok(extra_qemu_args)
}
pub(crate) fn prepare_sh_case_assets_sync(
case: &StarryQemuCase,
case_rootfs: &Path,
layout: &CaseAssetLayout,
) -> anyhow::Result<()> {
let sh_dir = case_sh_source_dir(case);
ensure!(
sh_dir.is_dir(),
"sh directory not found at `{}`",
sh_dir.display()
);
reset_dir(&layout.overlay_dir)?;
let dest_dir = layout.overlay_dir.join("usr/bin");
fs::create_dir_all(&dest_dir)
.with_context(|| format!("failed to create {}", dest_dir.display()))?;
let mut entries = fs::read_dir(&sh_dir)
.with_context(|| format!("failed to read {}", sh_dir.display()))?
.collect::<Result<Vec<_>, _>>()
.with_context(|| format!("failed to read {}", sh_dir.display()))?;
entries.sort_by_key(|e| e.file_name());
for entry in entries {
let path = entry.path();
if !path.is_file() {
continue;
}
let dest = dest_dir.join(entry.file_name());
fs::copy(&path, &dest)
.with_context(|| format!("failed to copy {} to {}", path.display(), dest.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(&dest)
.with_context(|| format!("failed to stat {}", dest.display()))?
.permissions();
perms.set_mode(0o755);
fs::set_permissions(&dest, perms)
.with_context(|| format!("failed to chmod {}", dest.display()))?;
}
}
crate::rootfs::inject::inject_overlay(case_rootfs, &layout.overlay_dir)
}
pub(crate) fn usb_qemu_args(usb_stick_path: &Path) -> Vec<String> {
vec![
"-device".to_string(),
"qemu-xhci,id=xhci".to_string(),
"-drive".to_string(),
format!(
"if=none,format=raw,file={},id=usbstick0",
usb_stick_path.display()
),
"-device".to_string(),
"usb-storage,drive=usbstick0,bus=xhci.0".to_string(),
]
}
pub(crate) fn create_usb_backing_image(path: &Path) -> anyhow::Result<()> {
let file =
fs::File::create(path).with_context(|| format!("failed to create {}", path.display()))?;
file.set_len(USB_STICK_IMAGE_SIZE)
.with_context(|| format!("failed to size {}", path.display()))
}
pub(crate) fn reset_dir(path: &Path) -> anyhow::Result<()> {
if path.exists() {
fs::remove_dir_all(path).with_context(|| format!("failed to remove {}", path.display()))?;
}
fs::create_dir_all(path).with_context(|| format!("failed to create {}", path.display()))
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use super::*;
fn fake_case(root: &Path, name: &str) -> StarryQemuCase {
let case_dir = root.join("test-suit/starryos/normal").join(name);
fs::create_dir_all(&case_dir).unwrap();
StarryQemuCase {
name: name.to_string(),
case_dir: case_dir.clone(),
qemu_config_path: case_dir.join("qemu-aarch64.toml"),
build_config_path: None,
}
}
#[test]
fn resolve_target_dir_uses_workspace_target_directory() {
let root = tempdir().unwrap();
let dir = resolve_target_dir(root.path(), "x86_64-unknown-none").unwrap();
assert_eq!(dir, root.path().join("target/x86_64-unknown-none"));
}
#[tokio::test]
async fn prepare_case_assets_keeps_default_cases_plain() {
let root = tempdir().unwrap();
let target_dir = root.path().join("target/x86_64-unknown-none");
let rootfs_dir = root.path().join("target/rootfs");
fs::create_dir_all(&target_dir).unwrap();
fs::create_dir_all(&rootfs_dir).unwrap();
fs::write(rootfs_dir.join("rootfs-x86_64-alpine.img"), b"rootfs").unwrap();
let case = fake_case(root.path(), "smoke");
let assets = prepare_case_assets(
root.path(),
"x86_64",
"x86_64-unknown-none",
&case,
rootfs_dir.join("rootfs-x86_64-alpine.img"),
)
.await
.unwrap();
assert_eq!(
assets.rootfs_path,
rootfs_dir.join("rootfs-x86_64-alpine.img")
);
assert!(assets.extra_qemu_args.is_empty());
assert_eq!(fs::read(&assets.rootfs_path).unwrap(), b"rootfs");
assert!(!target_dir.join("rootfs-x86_64-smoke.img").exists());
}
#[test]
fn case_asset_layout_and_usb_qemu_args_use_stable_paths() {
let root = tempdir().unwrap();
let layout =
case_asset_layout(root.path(), "aarch64-unknown-none-softfloat", "usb").unwrap();
assert_eq!(
layout.work_dir,
root.path()
.join("target/aarch64-unknown-none-softfloat/starry-cases/usb")
);
assert_eq!(
usb_qemu_args(&layout.usb_stick_path),
vec![
"-device".to_string(),
"qemu-xhci,id=xhci".to_string(),
"-drive".to_string(),
format!(
"if=none,format=raw,file={},id=usbstick0",
layout.usb_stick_path.display()
),
"-device".to_string(),
"usb-storage,drive=usbstick0,bus=xhci.0".to_string(),
]
);
}
}