axbuild 0.4.15

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

use anyhow::{Context, bail};
use toml::Value;

use super::refs::git_safe_directory_args;

pub(super) const ROOT_MANIFEST: &str = "Cargo.toml";
const WORKSPACE_TABLE: &str = "workspace";
const WORKSPACE_DEPENDENCIES_TABLE: &str = "dependencies";

#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) enum RootManifestChange {
    Hard,
    LocalWorkspaceDependencies(BTreeSet<String>),
}

pub(super) fn root_manifest_change_since(
    workspace_root: &Path,
    since: &str,
) -> anyhow::Result<RootManifestChange> {
    let old_manifest = git_show_file(workspace_root, since, ROOT_MANIFEST).with_context(|| {
        format!("failed to read `{ROOT_MANIFEST}` from `{since}` for incremental clippy")
    })?;
    let new_manifest =
        fs::read_to_string(workspace_root.join(ROOT_MANIFEST)).with_context(|| {
            format!(
                "failed to read current `{}`",
                workspace_root.join(ROOT_MANIFEST).display()
            )
        })?;

    classify_root_manifest_change(&old_manifest, &new_manifest).with_context(|| {
        format!("failed to classify `{ROOT_MANIFEST}` changes for incremental clippy")
    })
}

fn git_show_file(workspace_root: &Path, rev: &str, path: &str) -> anyhow::Result<String> {
    let spec = format!("{rev}:{path}");
    let output = Command::new("git")
        .args(git_safe_directory_args(workspace_root))
        .arg("-C")
        .arg(workspace_root)
        .args(["show", spec.as_str()])
        .output()
        .with_context(|| format!("failed to run git show `{spec}`"))?;
    if !output.status.success() {
        let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
        bail!(
            "git show `{spec}` exited with status {}{}",
            output.status,
            if stderr.is_empty() {
                String::new()
            } else {
                format!(": {stderr}")
            }
        );
    }

    String::from_utf8(output.stdout).context("git show output was not valid UTF-8")
}

pub(super) fn classify_root_manifest_change(
    old_manifest: &str,
    new_manifest: &str,
) -> anyhow::Result<RootManifestChange> {
    let old: Value = toml::from_str(old_manifest).context("failed to parse old root Cargo.toml")?;
    let new: Value = toml::from_str(new_manifest).context("failed to parse new root Cargo.toml")?;

    if without_workspace_dependencies(old.clone()) != without_workspace_dependencies(new.clone()) {
        return Ok(RootManifestChange::Hard);
    }

    let old_dependencies = workspace_dependencies(&old);
    let new_dependencies = workspace_dependencies(&new);
    let mut changed = BTreeSet::new();
    for dependency_key in old_dependencies
        .keys()
        .chain(new_dependencies.keys())
        .collect::<BTreeSet<_>>()
    {
        let old_dependency = old_dependencies.get(dependency_key.as_str());
        let new_dependency = new_dependencies.get(dependency_key.as_str());
        if old_dependency == new_dependency {
            continue;
        }
        let old_package = old_dependency.and_then(|dependency| {
            local_workspace_dependency_package_name(dependency_key, dependency)
        });
        let new_package = new_dependency.and_then(|dependency| {
            local_workspace_dependency_package_name(dependency_key, dependency)
        });
        if old_package.is_none() && new_package.is_none() {
            return Ok(RootManifestChange::Hard);
        }
        changed.extend(old_package);
        changed.extend(new_package);
    }

    Ok(RootManifestChange::LocalWorkspaceDependencies(changed))
}

fn without_workspace_dependencies(mut manifest: Value) -> Value {
    if let Some(workspace) = manifest
        .get_mut(WORKSPACE_TABLE)
        .and_then(Value::as_table_mut)
    {
        workspace.remove(WORKSPACE_DEPENDENCIES_TABLE);
    }
    manifest
}

fn workspace_dependencies(manifest: &Value) -> toml::Table {
    manifest
        .get(WORKSPACE_TABLE)
        .and_then(|workspace| workspace.get(WORKSPACE_DEPENDENCIES_TABLE))
        .and_then(Value::as_table)
        .cloned()
        .unwrap_or_default()
}

fn is_local_workspace_dependency(dependency: &Value) -> bool {
    dependency
        .as_table()
        .and_then(|table| table.get("path"))
        .is_some_and(Value::is_str)
}

fn local_workspace_dependency_package_name(
    dependency_key: &str,
    dependency: &Value,
) -> Option<String> {
    if !is_local_workspace_dependency(dependency) {
        return None;
    }

    Some(
        dependency
            .as_table()
            .and_then(|table| table.get("package"))
            .and_then(Value::as_str)
            .unwrap_or(dependency_key)
            .to_string(),
    )
}