axbuild 0.4.19

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

use anyhow::{Context, anyhow, bail};
use cargo_metadata::{Metadata, Package};
use walkdir::WalkDir;

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum SyncLintSelection {
    All { reason: Option<String> },
    Files(Vec<PathBuf>),
}

pub(super) fn workspace_packages(metadata: &Metadata) -> Vec<Package> {
    let workspace_members: HashSet<_> = metadata.workspace_members.iter().cloned().collect();
    let mut packages: Vec<_> = metadata
        .packages
        .iter()
        .filter(|pkg| workspace_members.contains(&pkg.id))
        .cloned()
        .collect();
    packages.sort_by(|left, right| left.name.cmp(&right.name));
    packages
}

fn package_dir(package: &Package) -> anyhow::Result<PathBuf> {
    let package_dir = package
        .manifest_path
        .clone()
        .into_std_path_buf()
        .parent()
        .map(Path::to_path_buf)
        .ok_or_else(|| anyhow!("invalid manifest path for package `{}`", package.name))?;
    Ok(package_dir)
}

pub(super) fn workspace_rust_source_files(packages: &[Package]) -> anyhow::Result<Vec<PathBuf>> {
    let mut files = BTreeSet::new();
    for package in packages {
        files.extend(rust_source_files(&package_dir(package)?));
    }
    Ok(files.into_iter().collect())
}

pub(super) fn select_sync_lint_files(
    workspace_root: &Path,
    packages: &[Package],
    since: Option<&str>,
) -> anyhow::Result<SyncLintSelection> {
    let Some(since) = since else {
        return Ok(SyncLintSelection::All { reason: None });
    };

    let changed_paths = match crate::support::git::changed_paths_since(workspace_root, since) {
        Ok(paths) => paths,
        Err(err) => {
            return Ok(SyncLintSelection::All {
                reason: Some(format!("failed to diff against `{since}`: {err:#}")),
            });
        }
    };

    select_sync_lint_files_for_paths(workspace_root, packages, changed_paths)
}

pub(super) fn select_sync_lint_files_for_paths<I>(
    workspace_root: &Path,
    packages: &[Package],
    changed_paths: I,
) -> anyhow::Result<SyncLintSelection>
where
    I: IntoIterator<Item = PathBuf>,
{
    let package_dirs = workspace_package_dirs(workspace_root, packages)?;
    let mut files = BTreeSet::new();

    for path in changed_paths {
        let path = normalize_changed_path(&path)?;
        if path.as_os_str().is_empty() {
            continue;
        }
        if !path.extension().is_some_and(|ext| ext == "rs") {
            continue;
        }
        let Some(_package_dir) = package_dir_for_path(&package_dirs, &path) else {
            return Ok(SyncLintSelection::All {
                reason: Some(format!(
                    "changed Rust path `{}` is outside any workspace package",
                    path.display()
                )),
            });
        };
        let absolute = workspace_root.join(&path);
        if absolute.is_file() {
            files.insert(absolute);
        }
    }

    Ok(SyncLintSelection::Files(files.into_iter().collect()))
}

fn workspace_package_dirs(
    workspace_root: &Path,
    packages: &[Package],
) -> anyhow::Result<Vec<PathBuf>> {
    let workspace_root = workspace_root
        .canonicalize()
        .with_context(|| format!("failed to canonicalize {}", workspace_root.display()))?;
    let mut dirs = packages
        .iter()
        .map(|package| {
            let manifest = package.manifest_path.clone().into_std_path_buf();
            let dir = manifest
                .parent()
                .ok_or_else(|| anyhow!("invalid manifest path for package `{}`", package.name))?;
            dir.strip_prefix(&workspace_root)
                .map(Path::to_path_buf)
                .with_context(|| {
                    format!(
                        "workspace package `{}` manifest {} is outside workspace root {}",
                        package.name,
                        manifest.display(),
                        workspace_root.display()
                    )
                })
        })
        .collect::<anyhow::Result<Vec<_>>>()?;
    dirs.sort_by(|left, right| {
        right
            .components()
            .count()
            .cmp(&left.components().count())
            .then_with(|| left.cmp(right))
    });
    Ok(dirs)
}

fn package_dir_for_path<'a>(package_dirs: &'a [PathBuf], path: &Path) -> Option<&'a Path> {
    package_dirs
        .iter()
        .find(|dir| path == dir.as_path() || path.starts_with(dir))
        .map(PathBuf::as_path)
}

fn normalize_changed_path(path: &Path) -> anyhow::Result<PathBuf> {
    if path.is_absolute() {
        bail!(
            "git diff returned absolute path `{}`; expected workspace-relative path",
            path.display()
        );
    }
    let mut normalized = PathBuf::new();
    for component in path.components() {
        match component {
            std::path::Component::Normal(part) => normalized.push(part),
            std::path::Component::CurDir => {}
            _ => bail!("invalid changed path `{}`", path.display()),
        }
    }
    Ok(normalized)
}

fn rust_source_files(package_dir: &Path) -> Vec<PathBuf> {
    let mut files = Vec::new();
    for entry in WalkDir::new(package_dir)
        .into_iter()
        .filter_entry(|entry| entry.file_name() != "target")
        .filter_map(Result::ok)
    {
        let path = entry.path();
        if path.is_file() && path.extension().is_some_and(|ext| ext == "rs") {
            files.push(path.to_path_buf());
        }
    }
    files.sort();
    files
}