cpu-local 0.2.0

Architecture CPU-local register capability for TGOSKits
Documentation
use std::{collections::BTreeMap, env, path::Path, process::Command};

const STARRY_TARGETS: &[&str] = &[
    "x86_64-unknown-none",
    "aarch64-unknown-none-softfloat",
    "riscv64gc-unknown-none-elf",
    "loongarch64-unknown-none-softfloat",
];

const AXVISOR_TARGETS: &[&str] = &[
    "scripts/targets/std/pie/x86_64-unknown-linux-musl.json",
    "scripts/targets/std/pie/aarch64-unknown-linux-musl.json",
    "scripts/targets/std/pie/riscv64gc-unknown-linux-musl.json",
    "scripts/targets/std/pie/loongarch64-unknown-linux-musl.json",
];

const TLS_CHAIN: &[&str] = &[
    "ax-std",
    "ax-runtime",
    "ax-hal",
    "cpu-local",
    "axplat-dyn",
    "somehal",
    "someboot",
];

#[test]
fn axvisor_final_images_select_the_complete_unikernel_tls_chain() {
    let workspace = workspace_root();

    for target in AXVISOR_TARGETS {
        let feature_graph = resolved_features(&workspace, "axvisor", target);
        for package in TLS_CHAIN {
            assert_has_feature(&feature_graph, package, "tls", "Axvisor", target);
        }
        assert_has_feature(&feature_graph, "axvm", "tls", "Axvisor", target);
    }
}

#[test]
fn starry_final_images_leave_kernel_tls_disabled() {
    let workspace = workspace_root();

    for target in STARRY_TARGETS {
        let feature_graph = resolved_features(&workspace, "starryos", target);
        for package in TLS_CHAIN {
            assert_lacks_feature(&feature_graph, package, "tls", "StarryOS", target);
        }
    }
}

#[test]
fn non_aarch64_final_images_never_select_arm_el2() {
    let workspace = workspace_root();

    for (package, targets) in [("axvisor", AXVISOR_TARGETS), ("starryos", STARRY_TARGETS)] {
        for target in targets.iter().filter(|target| !target.contains("aarch64-")) {
            let feature_graph = resolved_features(&workspace, package, target);
            assert_lacks_feature(&feature_graph, "ax-cpu", "arm-el2", package, target);
        }
    }
}

#[test]
fn aarch64_axvisor_selects_the_el2_cpu_execution_contract() {
    let workspace = workspace_root();
    let target = "scripts/targets/std/pie/aarch64-unknown-linux-musl.json";
    let feature_graph = resolved_features(&workspace, "axvisor", target);

    assert_has_feature(&feature_graph, "ax-cpu", "arm-el2", "Axvisor", target);
}

fn resolved_features(
    workspace: &Path,
    image_package: &str,
    target: &str,
) -> BTreeMap<String, Vec<String>> {
    let cargo = env::var_os("CARGO").unwrap_or_else(|| "cargo".into());
    let mut command = Command::new(cargo);
    command.current_dir(workspace).args([
        "tree",
        "--quiet",
        "-p",
        image_package,
        "--target",
        target,
        "-e",
        "normal,build",
        "--prefix",
        "none",
        "--format",
        "{p}|{f}",
    ]);
    if target.ends_with(".json") {
        command.args(["-Z", "json-target-spec"]);
    }
    let output = command
        .output()
        .unwrap_or_else(|error| panic!("failed to inspect {image_package}/{target}: {error}"));
    assert!(
        output.status.success(),
        "failed to inspect {image_package}/{target}:\n{}",
        String::from_utf8_lossy(&output.stderr)
    );

    let stdout = String::from_utf8(output.stdout).expect("cargo tree output must be UTF-8");
    let mut graph = BTreeMap::<String, Vec<String>>::new();
    for line in stdout.lines() {
        let Some((package, features)) = line.split_once('|') else {
            continue;
        };
        let Some((package_name, _)) = package.split_once(" v") else {
            continue;
        };
        let package_features = graph.entry(package_name.to_owned()).or_default();
        package_features.extend(
            features
                .trim_end_matches(" (*)")
                .split(',')
                .filter(|feature| !feature.is_empty())
                .map(str::to_owned),
        );
        package_features.sort_unstable();
        package_features.dedup();
    }
    graph
}

fn assert_has_feature(
    graph: &BTreeMap<String, Vec<String>>,
    package: &str,
    feature: &str,
    image: &str,
    target: &str,
) {
    let features = graph
        .get(package)
        .unwrap_or_else(|| panic!("{image}/{target} must resolve {package}"));
    assert!(
        features.iter().any(|resolved| resolved == feature),
        "{image}/{target} must select {package}/{feature}; resolved features: {features:?}"
    );
}

fn assert_lacks_feature(
    graph: &BTreeMap<String, Vec<String>>,
    package: &str,
    feature: &str,
    image: &str,
    target: &str,
) {
    let features = graph
        .get(package)
        .unwrap_or_else(|| panic!("{image}/{target} must resolve {package}"));
    assert!(
        features.iter().all(|resolved| resolved != feature),
        "{image}/{target} must not select {package}/{feature}; resolved features: {features:?}"
    );
}

fn workspace_root() -> std::path::PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .nth(2)
        .expect("cpu-local must be nested under the workspace components directory")
        .to_path_buf()
}