axbuild 0.4.6

An OS build lib toolkit used by arceos
use std::{fs, path::PathBuf};

use anyhow::Context;
use cargo_metadata::Metadata;
use log::warn;
use ostool::build::config::Cargo;
pub use ostool::build::config::LogLevel;

use crate::{
    build::{self, BuildInfo},
    context::ResolvedBuildRequest,
};

pub type ArceosBuildInfo = BuildInfo;

pub(crate) fn resolve_build_info_path(
    package: &str,
    target: &str,
    explicit_path: Option<PathBuf>,
) -> anyhow::Result<PathBuf> {
    if let Some(path) = explicit_path {
        return Ok(path);
    }

    default_build_info_path(package, target)
}

#[cfg(test)]
fn load_build_info(request: &ResolvedBuildRequest) -> anyhow::Result<ArceosBuildInfo> {
    let makefile_features = build::makefile_features_from_env();
    load_build_info_with_makefile_features(request, &makefile_features)
}

#[cfg(test)]
fn load_build_info_with_makefile_features(
    request: &ResolvedBuildRequest,
    makefile_features: &[String],
) -> anyhow::Result<ArceosBuildInfo> {
    let metadata = if makefile_features.is_empty() {
        None
    } else {
        Some(build::workspace_metadata().context("failed to load workspace metadata")?)
    };
    load_build_info_with_makefile_features_and_metadata(
        request,
        makefile_features,
        metadata.as_ref(),
    )
}

fn load_build_info_with_makefile_features_and_metadata(
    request: &ResolvedBuildRequest,
    makefile_features: &[String],
    metadata: Option<&Metadata>,
) -> anyhow::Result<ArceosBuildInfo> {
    build::ensure_build_info(&request.build_info_path, || {
        ArceosBuildInfo::default_for_target(&request.target)
    })?;
    let mut build_info: ArceosBuildInfo = build::load_build_info(&request.build_info_path)?;

    if build_info.normalize_legacy_feature_aliases() {
        warn!(
            "normalizing legacy feature aliases in build config {}",
            request.build_info_path.display()
        );
        fs::write(
            &request.build_info_path,
            toml::to_string_pretty(&build_info)?,
        )
        .with_context(|| {
            format!(
                "failed to rewrite normalized build info {}",
                request.build_info_path.display()
            )
        })?;
    }

    match metadata {
        Some(metadata) => build::apply_makefile_features_with_metadata(
            &mut build_info,
            &request.package,
            makefile_features,
            metadata,
        ),
        None => {
            build::apply_makefile_features(&mut build_info, &request.package, makefile_features)
        }
    }

    if let Some(smp) = request.smp {
        build_info.max_cpu_num = Some(smp);
    }

    Ok(build_info)
}

pub(crate) fn load_cargo_config(request: &ResolvedBuildRequest) -> anyhow::Result<Cargo> {
    let metadata =
        build::cached_workspace_metadata().context("failed to load workspace metadata")?;
    let makefile_features = build::makefile_features_from_env();
    let build_info = load_build_info_with_makefile_features_and_metadata(
        request,
        &makefile_features,
        Some(metadata),
    )?;

    build_info.into_prepared_base_cargo_config_with_metadata(
        &request.package,
        &request.target,
        request.plat_dyn,
        metadata,
    )
}

pub(crate) fn default_build_info_path(package: &str, target: &str) -> anyhow::Result<PathBuf> {
    Ok(build::default_build_info_path_in_workspace(
        &crate::context::workspace_root_path()?,
        package,
        target,
    ))
}

#[cfg(test)]
fn resolve_build_info_path_in_dir(dir: &std::path::Path, target: &str) -> PathBuf {
    let bare_path = dir.join(format!("build-{target}.toml"));
    if bare_path.exists() {
        return bare_path;
    }

    let dotted_path = dir.join(format!(".build-{target}.toml"));
    if dotted_path.exists() {
        return dotted_path;
    }

    dotted_path
}

#[cfg(test)]
mod tests {
    use std::fs;

    use tempfile::tempdir;

    use super::*;

    fn repo_metadata() -> cargo_metadata::Metadata {
        build::workspace_metadata().unwrap()
    }

    fn request(
        package: &str,
        target: &str,
        plat_dyn: Option<bool>,
        build_info_path: PathBuf,
    ) -> ResolvedBuildRequest {
        ResolvedBuildRequest {
            package: package.to_string(),
            arch: if target.starts_with("x86_64") {
                "x86_64".to_string()
            } else if target.starts_with("aarch64") {
                "aarch64".to_string()
            } else if target.starts_with("riscv64") {
                "riscv64".to_string()
            } else if target.starts_with("loongarch64") {
                "loongarch64".to_string()
            } else {
                "unknown".to_string()
            },
            target: target.to_string(),
            plat_dyn,
            smp: None,
            debug: false,
            build_info_path,
            qemu_config: None,
            uboot_config: None,
        }
    }

    #[test]
    fn resolves_dynamic_platform_features_and_args() {
        let mut build_info = ArceosBuildInfo::default_for_target("aarch64-unknown-none-softfloat");
        build_info.resolve_features("ax-helloworld", true);

        assert!(build_info.features.contains(&"ax-std/plat-dyn".to_string()));
        assert!(!build_info.features.contains(&"ax-std/defplat".to_string()));

        let args = ArceosBuildInfo::build_cargo_args("aarch64-unknown-none-softfloat", true, &[]);
        assert!(args.iter().any(|arg| arg.contains("-Taxplat.x")));
    }

    #[test]
    fn resolves_non_dynamic_platform_features_and_args() {
        let mut build_info = ArceosBuildInfo::default_for_target("aarch64-unknown-none-softfloat");
        build_info.resolve_features("ax-helloworld", false);

        assert!(build_info.features.contains(&"ax-std/defplat".to_string()));
        assert!(!build_info.features.contains(&"ax-std/plat-dyn".to_string()));

        let args = ArceosBuildInfo::build_cargo_args("aarch64-unknown-none-softfloat", false, &[]);
        assert!(args.iter().any(|arg| arg.contains("-Tlinker.x")));
    }

    #[test]
    fn max_cpu_num_adds_axfeat_smp_feature() {
        let metadata = repo_metadata();
        let mut build_info = ArceosBuildInfo {
            features: vec!["ax-feat/net".to_string()],
            max_cpu_num: Some(4),
            ..ArceosBuildInfo::default()
        };

        build_info.resolve_features_with_metadata("starryos", false, &metadata);

        assert!(build_info.features.contains(&"ax-feat/smp".to_string()));
    }

    #[test]
    fn resolve_build_info_path_uses_package_directory() {
        let path = resolve_build_info_path("ax-helloworld", "aarch64-unknown-none-softfloat", None)
            .unwrap();

        assert!(path.ends_with(
            "tmp/axbuild/config/ax-helloworld/build-aarch64-unknown-none-softfloat.toml"
        ));
    }

    #[test]
    fn resolve_build_info_path_prefers_explicit_path() {
        let path = resolve_build_info_path(
            "ax-helloworld",
            "aarch64-unknown-none-softfloat",
            Some(PathBuf::from("/tmp/custom-build.toml")),
        )
        .unwrap();

        assert_eq!(path, PathBuf::from("/tmp/custom-build.toml"));
    }

    #[test]
    fn resolve_build_info_path_in_dir_prefers_existing_bare_name() {
        let root = tempdir().unwrap();
        let bare = root
            .path()
            .join("build-aarch64-unknown-none-softfloat.toml");
        let dotted = root
            .path()
            .join(".build-aarch64-unknown-none-softfloat.toml");
        fs::write(&bare, "").unwrap();
        fs::write(&dotted, "").unwrap();

        let path = resolve_build_info_path_in_dir(root.path(), "aarch64-unknown-none-softfloat");

        assert_eq!(path, bare);
    }

    #[test]
    fn load_build_info_creates_missing_default_file() {
        let root = tempdir().unwrap();
        let path = root.path().join(".build-target.toml");
        let request = request("ax-helloworld", "target", None, path.clone());

        let build_info = load_build_info(&request).unwrap();

        assert_eq!(build_info, ArceosBuildInfo::default_for_target("target"));
        assert!(path.exists());
        assert!(
            fs::read_to_string(path)
                .unwrap()
                .contains("features = [\"ax-std\"]")
        );
    }

    #[test]
    fn load_build_info_normalizes_legacy_feature_aliases() {
        let root = tempdir().unwrap();
        let path = root.path().join(".build-target.toml");
        fs::write(
            &path,
            r#"
features = ["axstd", "axstd/smp", "axfeat/net"]
log = "Warn"

[env]
AX_IP = "10.0.2.15"
"#,
        )
        .unwrap();
        let request = request("ax-helloworld", "target", None, path.clone());

        let build_info = load_build_info(&request).unwrap();

        assert!(build_info.features.contains(&"ax-std".to_string()));
        assert!(build_info.features.contains(&"ax-std/smp".to_string()));
        assert!(build_info.features.contains(&"ax-feat/net".to_string()));
        assert!(!build_info.features.contains(&"axstd".to_string()));

        let rewritten = fs::read_to_string(path).unwrap();
        assert!(rewritten.contains("ax-std"));
        assert!(!rewritten.contains("axstd"));
    }

    #[test]
    fn parse_makefile_features_splits_commas_whitespace_and_dedups() {
        assert_eq!(
            build::parse_makefile_features(" lockdep, sched-rr  lockdep\taxfeat/net "),
            vec![
                "lockdep".to_string(),
                "sched-rr".to_string(),
                "axfeat/net".to_string()
            ]
        );
    }

    #[test]
    fn apply_makefile_features_uses_axfeat_prefix_for_axfeat_packages() {
        let metadata = repo_metadata();
        let mut build_info = ArceosBuildInfo {
            features: Vec::new(),
            ..ArceosBuildInfo::default()
        };

        build::apply_makefile_features_with_metadata(
            &mut build_info,
            "starryos",
            &[String::from("lockdep")],
            &metadata,
        );

        assert!(build_info.features.contains(&"ax-feat/lockdep".to_string()));
        assert!(!build_info.features.contains(&"ax-std/lockdep".to_string()));
    }

    #[test]
    fn to_cargo_config_maps_max_cpu_num_to_smp_env_for_dynamic_platforms() {
        let root = tempdir().unwrap();
        let request = request(
            "ax-helloworld",
            "aarch64-unknown-none-softfloat",
            Some(true),
            root.path().join(".build.toml"),
        );

        let metadata = repo_metadata();
        let cargo = ArceosBuildInfo {
            max_cpu_num: Some(4),
            ..ArceosBuildInfo::default_for_target("aarch64-unknown-none-softfloat")
        }
        .into_prepared_base_cargo_config_with_metadata(
            &request.package,
            &request.target,
            request.plat_dyn,
            &metadata,
        )
        .unwrap();

        assert_eq!(cargo.env.get("SMP"), Some(&"4".to_string()));
        assert!(cargo.features.contains(&"ax-std/smp".to_string()));
    }

    #[test]
    fn base_cargo_config_defaults_to_bin_false_for_x86_64_targets() {
        let cargo = ArceosBuildInfo::default_for_target("x86_64-unknown-none")
            .into_base_cargo_config_with_log(
                "ax-helloworld".to_string(),
                "x86_64-unknown-none".to_string(),
                vec![],
            );

        assert!(!cargo.to_bin);
    }

    #[test]
    fn resolve_effective_plat_dyn_uses_override_and_target_support() {
        assert!(build::resolve_effective_plat_dyn(
            "aarch64-unknown-none-softfloat",
            true,
            None
        ));
        assert!(!build::resolve_effective_plat_dyn(
            "aarch64-unknown-none-softfloat",
            true,
            Some(false)
        ));
        assert!(!build::resolve_effective_plat_dyn(
            "x86_64-unknown-none",
            true,
            Some(true)
        ));
    }
}