axbuild 0.4.15

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

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

use super::{
    build_config::collect_prefixed_toml_files,
    types::{StarryAppCase, StarryAppKind},
};

pub(crate) fn discover_apps(workspace_root: &Path) -> anyhow::Result<Vec<StarryAppCase>> {
    discover_apps_with_ignore(workspace_root, true)
}

pub(super) fn discover_apps_with_ignore(
    workspace_root: &Path,
    respect_ignore: bool,
) -> anyhow::Result<Vec<StarryAppCase>> {
    let apps_dir = apps_starry_dir(workspace_root);
    ensure!(
        apps_dir.is_dir(),
        "missing Starry apps directory `{}`",
        apps_dir.display()
    );

    let ignored = if respect_ignore {
        ignored_app_names(workspace_root)?
    } else {
        BTreeSet::new()
    };
    let mut apps = Vec::new();
    collect_apps_in_dir(&apps_dir, &apps_dir, &ignored, &mut apps)?;
    apps.sort_by(|a, b| a.name.cmp(&b.name));
    Ok(apps)
}

fn collect_apps_in_dir(
    apps_dir: &Path,
    dir: &Path,
    ignored: &BTreeSet<String>,
    apps: &mut Vec<StarryAppCase>,
) -> anyhow::Result<()> {
    for entry in fs::read_dir(dir).with_context(|| format!("failed to read {}", dir.display()))? {
        let entry = entry?;
        let case_dir = entry.path();
        if !case_dir.is_dir() {
            continue;
        }
        let name = relative_app_name(apps_dir, &case_dir)?;
        if is_ignored_app(ignored, &name) {
            continue;
        }
        if let Some(kind) = infer_app_kind(&case_dir)? {
            apps.push(StarryAppCase {
                name,
                kind,
                prebuild_path: optional_file(case_dir.join("prebuild.sh")),
                requires: read_requires(&case_dir)?,
                case_dir,
            });
            continue;
        }
        collect_apps_in_dir(apps_dir, &case_dir, ignored, apps)?;
    }
    Ok(())
}

fn relative_app_name(apps_dir: &Path, case_dir: &Path) -> anyhow::Result<String> {
    let relative = case_dir.strip_prefix(apps_dir).with_context(|| {
        format!(
            "failed to make {} relative to {}",
            case_dir.display(),
            apps_dir.display()
        )
    })?;
    Ok(relative
        .components()
        .filter_map(|component| match component {
            std::path::Component::Normal(name) => name.to_str(),
            _ => None,
        })
        .collect::<Vec<_>>()
        .join("/"))
}

fn optional_file(path: PathBuf) -> Option<PathBuf> {
    path.is_file().then_some(path)
}

fn ignored_app_names(workspace_root: &Path) -> anyhow::Result<BTreeSet<String>> {
    let path = workspace_root.join("apps/.ignore");
    if !path.is_file() {
        return Ok(BTreeSet::new());
    }
    let content =
        fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
    Ok(content
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty() && !line.starts_with('#'))
        .map(|line| line.trim_matches('/').to_string())
        .collect())
}

fn is_ignored_app(ignored: &BTreeSet<String>, name: &str) -> bool {
    ignored.contains(name)
        || ignored.contains(&format!("starry/{name}"))
        || ignored.contains(&format!("apps/starry/{name}"))
}

fn infer_app_kind(case_dir: &Path) -> anyhow::Result<Option<StarryAppKind>> {
    let has_qemu = !collect_prefixed_toml_files(case_dir, "qemu-")?.is_empty();
    let has_board = case_dir.join("init.sh").is_file()
        && !collect_prefixed_toml_files(case_dir, "board-")?.is_empty();
    let has_prebuild = case_dir.join("prebuild.sh").is_file();

    match (has_qemu, has_board, has_prebuild) {
        (true, false, _) => Ok(Some(StarryAppKind::Qemu)),
        (false, true, _) => Ok(Some(StarryAppKind::Board)),
        (false, false, true) => Ok(Some(StarryAppKind::Qemu)),
        (false, false, false) => Ok(None),
        (true, true, _) => bail!(
            "Starry app `{}` has both qemu-* and board-* configs; split it or make kind explicit",
            case_dir.display()
        ),
    }
}

fn read_requires(case_dir: &Path) -> anyhow::Result<Vec<String>> {
    let path = case_dir.join("requires");
    if !path.is_file() {
        return Ok(Vec::new());
    }
    let content =
        fs::read_to_string(&path).with_context(|| format!("failed to read {}", path.display()))?;
    Ok(content
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty() && !line.starts_with('#'))
        .map(str::to_string)
        .collect())
}

pub(super) fn apps_starry_dir(workspace_root: &Path) -> PathBuf {
    workspace_root.join("apps/starry")
}

pub(super) fn validate_case_name(case_name: &str) -> anyhow::Result<&str> {
    let case_name = case_name.trim();
    ensure!(!case_name.is_empty(), "Starry app case name is empty");
    let path = Path::new(case_name);
    ensure!(
        !path.is_absolute()
            && path
                .components()
                .all(|component| matches!(component, std::path::Component::Normal(_))),
        "invalid Starry app case name `{case_name}`"
    );
    Ok(case_name)
}

pub(super) fn available_case_names(apps_dir: &Path) -> anyhow::Result<String> {
    let mut cases = Vec::new();
    for entry in
        fs::read_dir(apps_dir).with_context(|| format!("failed to read {}", apps_dir.display()))?
    {
        let entry = entry?;
        if !entry.path().is_dir() {
            continue;
        }
        let Ok(name) = entry.file_name().into_string() else {
            continue;
        };
        cases.push(name);
    }
    cases.sort();
    if cases.is_empty() {
        Ok("<none>".to_string())
    } else {
        Ok(cases.join(", "))
    }
}

pub(super) fn resolve_case_relative_path(case_dir: &Path, path: &Path) -> PathBuf {
    if path.is_absolute() {
        return path.to_path_buf();
    }

    let case_relative = case_dir.join(path);
    if case_relative.exists() {
        case_relative
    } else {
        path.to_path_buf()
    }
}

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