use std::{
fs,
path::{Path, PathBuf},
process::Command,
};
use anyhow::Context;
use ostool::build::config::Cargo;
pub type StarryBuildInfo = crate::arceos::build::ArceosBuildInfo;
pub use crate::arceos::build::LogLevel;
use crate::context::{
ResolvedStarryRequest, STARRY_PACKAGE, starry_arch_for_target_checked, workspace_member_dir_in,
};
impl StarryBuildInfo {
pub fn default_starry_for_target(target: &str) -> Self {
let mut build_info = Self::default_for_target(target);
build_info.plat_dyn = false;
build_info.features = vec!["qemu".to_string()];
build_info
}
}
pub fn resolve_build_info_path(
workspace_root: &Path,
target: &str,
explicit_path: Option<PathBuf>,
) -> anyhow::Result<PathBuf> {
if let Some(path) = explicit_path {
return Ok(path);
}
let _ = starry_arch_for_target_checked(target)?;
Ok(crate::arceos::build::resolve_build_info_path_in_dir(
&workspace_member_dir_in(workspace_root, STARRY_PACKAGE)?,
target,
))
}
pub fn load_build_info(request: &ResolvedStarryRequest) -> anyhow::Result<StarryBuildInfo> {
crate::arceos::build::load_or_create_build_info(&request.build_info_path, || {
StarryBuildInfo::default_starry_for_target(&request.target)
})
}
pub fn load_cargo_config(request: &ResolvedStarryRequest) -> anyhow::Result<Cargo> {
to_cargo_config(load_build_info(request)?, request)
}
const ROOTFS_URL: &str = "https://github.com/Starry-OS/rootfs/releases/download/20260214";
pub fn rootfs_image_name(arch: &str) -> String {
format!("rootfs-{arch}.img")
}
pub fn rootfs_artifact_dir(workspace_root: &Path, target: &str) -> PathBuf {
workspace_root.join("target").join(target)
}
pub fn rootfs_disk_image_path(workspace_root: &Path, target: &str) -> PathBuf {
rootfs_artifact_dir(workspace_root, target).join("disk.img")
}
pub fn default_qemu_args(disk_img: &Path) -> Vec<String> {
vec![
"-device".to_string(),
"virtio-blk-pci,drive=disk0".to_string(),
"-drive".to_string(),
format!("id=disk0,if=none,format=raw,file={}", disk_img.display()),
"-device".to_string(),
"virtio-net-pci,netdev=net0".to_string(),
"-netdev".to_string(),
"user,id=net0,hostfwd=tcp::5555-:5555".to_string(),
]
}
pub fn ensure_rootfs_in_target_dir(
workspace_root: &Path,
arch: &str,
target: &str,
) -> anyhow::Result<PathBuf> {
let artifact_dir = rootfs_artifact_dir(workspace_root, target);
let disk_img = artifact_dir.join("disk.img");
let rootfs_name = rootfs_image_name(arch);
let rootfs_img = artifact_dir.join(&rootfs_name);
let rootfs_xz = artifact_dir.join(format!("{rootfs_name}.xz"));
fs::create_dir_all(&artifact_dir)
.with_context(|| format!("failed to create {}", artifact_dir.display()))?;
if !rootfs_img.exists() {
println!("image not found, downloading {}...", rootfs_name);
let url = format!("{ROOTFS_URL}/{rootfs_name}.xz");
let status = Command::new("curl")
.arg("-f")
.arg("-L")
.arg(&url)
.arg("-o")
.arg(&rootfs_xz)
.status()
.with_context(|| format!("failed to spawn curl for {url}"))?;
if !status.success() {
anyhow::bail!("failed to download {}", url);
}
let status = Command::new("xz")
.arg("-d")
.arg("-f")
.arg(&rootfs_xz)
.status()
.with_context(|| format!("failed to spawn xz for {}", rootfs_xz.display()))?;
if !status.success() {
anyhow::bail!("failed to decompress {}", rootfs_xz.display());
}
}
fs::copy(&rootfs_img, &disk_img).with_context(|| {
format!(
"failed to copy {} to {}",
rootfs_img.display(),
disk_img.display()
)
})?;
Ok(disk_img)
}
pub fn to_cargo_config(
build_info: StarryBuildInfo,
request: &ResolvedStarryRequest,
) -> anyhow::Result<Cargo> {
let mut cargo = build_info.into_prepared_base_cargo_config(
&request.package,
&request.target,
request.plat_dyn,
)?;
patch_starry_cargo_config(&mut cargo, request)?;
Ok(cargo)
}
fn patch_starry_cargo_config(
cargo: &mut Cargo,
request: &ResolvedStarryRequest,
) -> anyhow::Result<()> {
let platform = default_platform_for_arch(&request.arch)?;
cargo.package = request.package.clone();
cargo.target = request.target.clone();
ensure_starry_bin_arg(&mut cargo.args, &request.package);
rewrite_linker_script_arg(&mut cargo.args, request, platform)?;
cargo.features.push("qemu".to_string());
cargo.features.sort();
cargo.features.dedup();
cargo
.env
.insert("AX_ARCH".to_string(), request.arch.clone());
cargo
.env
.insert("AX_TARGET".to_string(), request.target.clone());
cargo
.env
.entry("AX_PLATFORM".to_string())
.or_insert_with(|| platform.to_string());
Ok(())
}
fn ensure_starry_bin_arg(args: &mut Vec<String>, package: &str) {
if args.iter().any(|arg| arg == "--bin") {
return;
}
args.push("--bin".to_string());
args.push(package.to_string());
}
fn rewrite_linker_script_arg(
args: &mut Vec<String>,
request: &ResolvedStarryRequest,
platform: &str,
) -> anyhow::Result<()> {
let linker_script = starry_linker_script_path(request, platform)?;
let needle = "-Clink-arg=-Tlinker.x";
let replacement = format!("-Clink-arg=-T{}", linker_script.display());
for arg in args {
if arg.contains(needle) {
*arg = arg.replace(needle, &replacement);
}
}
Ok(())
}
fn starry_linker_script_path(
request: &ResolvedStarryRequest,
platform: &str,
) -> anyhow::Result<PathBuf> {
let workspace_root = request
.build_info_path
.parent()
.and_then(Path::parent)
.context("Starry build info path should be nested under <workspace>/starryos")?;
Ok(workspace_root
.join("target")
.join(&request.target)
.join("release")
.join(format!("linker_{platform}.lds")))
}
fn default_platform_for_arch(arch: &str) -> anyhow::Result<&'static str> {
match arch {
"aarch64" => Ok("aarch64-qemu-virt"),
"x86_64" => Ok("x86-pc"),
"riscv64" => Ok("riscv64-qemu-virt"),
"loongarch64" => Ok("loongarch64-qemu-virt"),
_ => anyhow::bail!(
"unsupported Starry architecture `{arch}`; expected one of aarch64, x86_64, riscv64, \
loongarch64"
),
}
}
#[cfg(test)]
mod tests {
use std::{collections::HashMap, fs};
use tempfile::tempdir;
use super::*;
use crate::context::STARRY_PACKAGE;
fn write_minimal_package_manifest(path: &Path, name: &str) {
let src_dir = path.parent().unwrap().join("src");
fs::create_dir_all(&src_dir).unwrap();
fs::write(src_dir.join("lib.rs"), "").unwrap();
fs::write(
path,
format!("[package]\nname = \"{name}\"\nversion = \"0.1.0\"\nedition = \"2021\"\n"),
)
.unwrap();
}
fn request(path: PathBuf, arch: &str, target: &str) -> ResolvedStarryRequest {
ResolvedStarryRequest {
package: STARRY_PACKAGE.to_string(),
arch: arch.to_string(),
target: target.to_string(),
plat_dyn: None,
build_info_path: path,
qemu_config: None,
uboot_config: None,
}
}
#[test]
fn resolve_build_info_path_uses_default_starry_location() {
let root = tempdir().unwrap();
let starry_dir = root.path().join("os/StarryOS/starryos");
fs::create_dir_all(&starry_dir).unwrap();
write_minimal_package_manifest(&starry_dir.join("Cargo.toml"), STARRY_PACKAGE);
fs::write(
root.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"os/StarryOS/starryos\"]\n",
)
.unwrap();
let path =
resolve_build_info_path(root.path(), "aarch64-unknown-none-softfloat", None).unwrap();
assert_eq!(
path,
root.path()
.join("os/StarryOS/starryos/.build-aarch64-unknown-none-softfloat.toml")
);
}
#[test]
fn resolve_build_info_path_prefers_existing_bare_name() {
let root = tempdir().unwrap();
let starry_dir = root.path().join("os/StarryOS/starryos");
fs::create_dir_all(&starry_dir).unwrap();
write_minimal_package_manifest(&starry_dir.join("Cargo.toml"), STARRY_PACKAGE);
fs::write(
root.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"os/StarryOS/starryos\"]\n",
)
.unwrap();
let bare = starry_dir.join("build-aarch64-unknown-none-softfloat.toml");
let dotted = starry_dir.join(".build-aarch64-unknown-none-softfloat.toml");
fs::write(&bare, "").unwrap();
fs::write(&dotted, "").unwrap();
let path =
resolve_build_info_path(root.path(), "aarch64-unknown-none-softfloat", None).unwrap();
assert_eq!(path, bare);
}
#[test]
fn resolve_build_info_path_prefers_explicit_path() {
let root = tempdir().unwrap();
let starry_dir = root.path().join("os/StarryOS/starryos");
fs::create_dir_all(&starry_dir).unwrap();
write_minimal_package_manifest(&starry_dir.join("Cargo.toml"), STARRY_PACKAGE);
fs::write(
root.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"os/StarryOS/starryos\"]\n",
)
.unwrap();
let explicit = root.path().join("custom/build.toml");
let path =
resolve_build_info_path(root.path(), "x86_64-unknown-none", Some(explicit.clone()))
.unwrap();
assert_eq!(path, explicit);
}
#[test]
fn load_build_info_writes_default_template_when_missing() {
let root = tempdir().unwrap();
let path = root.path().join(".build-target.toml");
let request = request(path.clone(), "aarch64", "aarch64-unknown-none-softfloat");
let build_info = load_build_info(&request).unwrap();
assert_eq!(
build_info,
StarryBuildInfo::default_starry_for_target("aarch64-unknown-none-softfloat")
);
assert!(path.exists());
let persisted: StarryBuildInfo =
toml::from_str(&fs::read_to_string(path).unwrap()).unwrap();
assert_eq!(persisted, build_info);
}
#[test]
fn load_build_info_reads_existing_file() {
let root = tempdir().unwrap();
let path = root.path().join(".build-target.toml");
fs::write(
&path,
r#"
log = "Info"
features = ["net"]
[env]
HELLO = "world"
"#,
)
.unwrap();
let request = request(path, "aarch64", "aarch64-unknown-none-softfloat");
let build_info = load_build_info(&request).unwrap();
assert_eq!(build_info.log, LogLevel::Info);
assert_eq!(build_info.features, vec!["net".to_string()]);
assert_eq!(
build_info.env.get("HELLO").map(String::as_str),
Some("world")
);
}
#[test]
fn patch_starry_cargo_config_injects_required_features_and_env() {
let request = request(
PathBuf::from("/tmp/.build.toml"),
"aarch64",
"aarch64-unknown-none-softfloat",
);
let build_info = StarryBuildInfo {
env: HashMap::from([(String::from("CUSTOM"), String::from("1"))]),
features: vec!["net".to_string()],
log: LogLevel::Info,
max_cpu_num: None,
plat_dyn: false,
};
let mut cargo = build_info.into_base_cargo_config_with_log(
STARRY_PACKAGE.to_string(),
request.target.clone(),
vec![],
);
patch_starry_cargo_config(&mut cargo, &request).unwrap();
assert_eq!(cargo.package, STARRY_PACKAGE);
assert_eq!(cargo.target, "aarch64-unknown-none-softfloat");
assert_eq!(cargo.features, vec!["net".to_string(), "qemu".to_string()]);
assert_eq!(
cargo.env.get("AX_ARCH").map(String::as_str),
Some("aarch64")
);
assert_eq!(
cargo.env.get("AX_TARGET").map(String::as_str),
Some("aarch64-unknown-none-softfloat")
);
assert_eq!(
cargo.env.get("AX_PLATFORM").map(String::as_str),
Some("aarch64-qemu-virt")
);
assert_eq!(cargo.env.get("AX_LOG").map(String::as_str), Some("info"));
assert_eq!(cargo.env.get("CUSTOM").map(String::as_str), Some("1"));
assert!(cargo.to_bin);
}
#[test]
fn patch_starry_cargo_config_preserves_request_package() {
let request = ResolvedStarryRequest {
package: "starryos-test".to_string(),
arch: "x86_64".to_string(),
target: "x86_64-unknown-none".to_string(),
plat_dyn: None,
build_info_path: PathBuf::from("/tmp/.build.toml"),
qemu_config: None,
uboot_config: None,
};
let build_info = StarryBuildInfo::default_starry_for_target("x86_64-unknown-none");
let mut cargo = build_info.into_base_cargo_config_with_log(
"placeholder".to_string(),
request.target.clone(),
vec![],
);
patch_starry_cargo_config(&mut cargo, &request).unwrap();
assert_eq!(cargo.package, "starryos-test");
assert!(
cargo
.args
.windows(2)
.any(|window| window == ["--bin", "starryos-test"])
);
}
#[test]
fn rootfs_disk_image_path_uses_workspace_target_triple_dir() {
let root = Path::new("/tmp/workspace");
let disk_img = rootfs_disk_image_path(root, "aarch64-unknown-none-softfloat");
assert_eq!(
disk_img,
PathBuf::from("/tmp/workspace/target/aarch64-unknown-none-softfloat/disk.img")
);
}
#[test]
fn resolve_build_info_path_supports_starry_subworkspace_root() {
let root = tempdir().unwrap();
let starry_dir = root.path().join("starryos");
fs::create_dir_all(&starry_dir).unwrap();
write_minimal_package_manifest(&starry_dir.join("Cargo.toml"), STARRY_PACKAGE);
fs::write(
root.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"starryos\"]\n",
)
.unwrap();
let path =
resolve_build_info_path(root.path(), "aarch64-unknown-none-softfloat", None).unwrap();
assert_eq!(
path,
root.path()
.join("starryos/.build-aarch64-unknown-none-softfloat.toml")
);
}
#[test]
fn default_qemu_args_include_disk_and_network_defaults() {
let args = default_qemu_args(Path::new("/tmp/disk.img"));
assert_eq!(
args,
vec![
"-device".to_string(),
"virtio-blk-pci,drive=disk0".to_string(),
"-drive".to_string(),
"id=disk0,if=none,format=raw,file=/tmp/disk.img".to_string(),
"-device".to_string(),
"virtio-net-pci,netdev=net0".to_string(),
"-netdev".to_string(),
"user,id=net0,hostfwd=tcp::5555-:5555".to_string(),
]
);
}
#[test]
fn patch_starry_cargo_config_uses_absolute_linker_script_path() {
let request = ResolvedStarryRequest {
package: STARRY_PACKAGE.to_string(),
arch: "aarch64".to_string(),
target: "aarch64-unknown-none-softfloat".to_string(),
plat_dyn: None,
build_info_path: PathBuf::from(
"/tmp/os/StarryOS/starryos/.build-aarch64-unknown-none-softfloat.toml",
),
qemu_config: None,
uboot_config: None,
};
let build_info = StarryBuildInfo::default_starry_for_target(&request.target);
let mut cargo = build_info.into_base_cargo_config_with_log(
request.package.clone(),
request.target.clone(),
StarryBuildInfo::build_cargo_args(&request.target, false),
);
patch_starry_cargo_config(&mut cargo, &request).unwrap();
assert!(cargo.args.iter().any(|arg| arg.contains(
"/tmp/os/StarryOS/target/aarch64-unknown-none-softfloat/release/\
linker_aarch64-qemu-virt.lds"
)));
}
}