axbuild 0.4.16

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

use anyhow::{Context, bail, ensure};
use serde::Deserialize;

use super::super::board;

#[derive(Debug, Clone, PartialEq, Eq)]
struct BuildConfigCandidate {
    path: PathBuf,
    target: String,
}

#[derive(Debug, Deserialize)]
struct BuildConfigTarget {
    target: Option<String>,
}

pub(super) fn discover_optional_build_config(
    case_dir: &Path,
    target: &str,
) -> anyhow::Result<Option<PathBuf>> {
    let mut dir = Some(case_dir);
    while let Some(current_dir) = dir {
        if let Some(path) = resolve_exact_build_config_path(current_dir, target)? {
            return Ok(Some(path));
        }
        dir = current_dir.parent();
    }
    Ok(None)
}

fn resolve_exact_build_config_path(dir: &Path, target: &str) -> anyhow::Result<Option<PathBuf>> {
    let path = dir.join(format!("build-{target}.toml"));
    if path.is_file() {
        return Ok(Some(path));
    }

    let legacy_candidates = legacy_build_config_candidates(dir, target);
    if !legacy_candidates.is_empty() {
        bail!(
            "unsupported legacy build config name(s) under {}: {}; expected only              \
             `build-{target}.toml`",
            dir.display(),
            legacy_candidates
                .iter()
                .map(|path| path.display().to_string())
                .collect::<Vec<_>>()
                .join(", ")
        );
    }

    Ok(None)
}

fn legacy_build_config_candidates(dir: &Path, target: &str) -> Vec<PathBuf> {
    let Some(arch) = arch_from_target_name(target) else {
        return Vec::new();
    };
    [
        dir.join(format!(".build-{target}.toml")),
        dir.join(format!("build-{arch}.toml")),
        dir.join(format!(".build-{arch}.toml")),
    ]
    .into_iter()
    .filter(|path| path.is_file())
    .collect()
}

fn arch_from_target_name(target: &str) -> Option<&str> {
    target.split_once('-').map(|(arch, _)| arch)
}

pub(super) fn discover_case_build_config(
    case_dir: &Path,
    preferred_target: Option<&str>,
) -> anyhow::Result<(PathBuf, String)> {
    let mut candidates = collect_build_config_candidates(case_dir)?;
    ensure!(
        !candidates.is_empty(),
        "Starry app case `{}` does not provide a build-<target>.toml config",
        case_dir.display()
    );

    if let Some(preferred_target) = preferred_target
        && let Some(index) = candidates
            .iter()
            .position(|candidate| candidate.target == preferred_target)
    {
        let candidate = candidates.remove(index);
        return Ok((candidate.path, candidate.target));
    }

    match candidates.len() {
        1 => {
            let candidate = candidates.remove(0);
            Ok((candidate.path, candidate.target))
        }
        _ => bail!(
            "Starry app case `{}` provides multiple build configs; pass a board config that maps \
             to one target or keep one build config",
            case_dir.display()
        ),
    }
}

pub(super) fn collect_prefixed_toml_files(
    case_dir: &Path,
    prefix: &str,
) -> anyhow::Result<Vec<PathBuf>> {
    let mut configs = Vec::new();
    for entry in
        fs::read_dir(case_dir).with_context(|| format!("failed to read {}", case_dir.display()))?
    {
        let entry = entry?;
        let path = entry.path();
        if !path.is_file() || path.extension().is_none_or(|ext| ext != "toml") {
            continue;
        }
        let Some(stem) = path.file_stem().and_then(|stem| stem.to_str()) else {
            continue;
        };
        if stem.starts_with(prefix) {
            configs.push(path);
        }
    }
    configs.sort();
    Ok(configs)
}

fn collect_build_config_candidates(case_dir: &Path) -> anyhow::Result<Vec<BuildConfigCandidate>> {
    let mut paths = collect_prefixed_toml_files(case_dir, "build-")?;
    paths.extend(collect_prefixed_toml_files(case_dir, ".build-")?);
    paths.sort();
    paths.dedup();

    paths
        .into_iter()
        .map(|path| {
            let target = build_config_target(&path)?;
            Ok(BuildConfigCandidate { path, target })
        })
        .collect()
}

fn build_config_target(path: &Path) -> anyhow::Result<String> {
    let content =
        fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
    let parsed: BuildConfigTarget =
        toml::from_str(&content).with_context(|| format!("failed to parse {}", path.display()))?;
    let filename_target = path
        .file_stem()
        .and_then(|stem| stem.to_str())
        .and_then(build_config_target_from_stem);

    if let (Some(parsed), Some(filename)) = (parsed.target.as_deref(), filename_target.as_deref())
        && parsed != filename
    {
        bail!(
            "build config `{}` target `{parsed}` does not match filename target `{filename}`",
            path.display()
        );
    }

    parsed.target.or(filename_target).ok_or_else(|| {
        anyhow::anyhow!(
            "build config `{}` must define top-level `target` or use build-<target>.toml",
            path.display()
        )
    })
}

fn build_config_target_from_stem(stem: &str) -> Option<String> {
    stem.strip_prefix("build-")
        .or_else(|| stem.strip_prefix(".build-"))
        .map(str::to_string)
        .filter(|target| !target.is_empty())
}

pub(super) fn default_target_for_board_config(
    workspace_root: &Path,
    board_config_path: &Path,
) -> anyhow::Result<Option<String>> {
    let Some(stem) = board_config_path.file_stem().and_then(|stem| stem.to_str()) else {
        return Ok(None);
    };
    let Some(board_name) = stem.strip_prefix("board-") else {
        return Ok(None);
    };
    let build_config_path = workspace_root
        .join("os/StarryOS/configs/board")
        .join(format!("{board_name}.toml"));
    if !build_config_path.is_file() {
        return Ok(None);
    }
    Ok(Some(board::load_board_file(&build_config_path)?.target))
}

#[cfg(test)]
#[path = "tests/build_config.rs"]
mod tests;