plotnik-langs 0.3.1

Tree-sitter language bindings for Plotnik query language
Documentation
use std::path::{Path, PathBuf};

use serde::Serialize;

fn main() {
    let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
    let manifest_path = PathBuf::from(&manifest_dir).join("Cargo.toml");
    let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set"));

    let enabled_features: Vec<String> = std::env::vars()
        .filter_map(|(key, _)| {
            key.strip_prefix("CARGO_FEATURE_LANG_")
                .map(|suffix| format!("lang-{}", suffix.to_lowercase().replace('_', "-")))
        })
        .collect();

    if enabled_features.is_empty() {
        println!("cargo::rerun-if-changed=build.rs");
        println!("cargo::rerun-if-changed=Cargo.toml");
        return;
    }

    let metadata = cargo_metadata::MetadataCommand::new()
        .manifest_path(&manifest_path)
        .features(cargo_metadata::CargoOpt::SomeFeatures(enabled_features))
        .exec()
        .expect("failed to run cargo metadata");

    for package in &metadata.packages {
        if !package.name.starts_with("arborium-") {
            continue;
        }

        let Some(feature_name) = arborium_package_to_feature(&package.name) else {
            continue;
        };

        let package_root = package
            .manifest_path
            .parent()
            .expect("package has no parent dir");

        let lang_key = feature_to_lang_key(&feature_name);

        // Process grammar.json
        let grammar_path = package_root.join("grammar/src/grammar.json");
        if !grammar_path.exists() {
            panic!(
                "grammar.json not found for {}: {}",
                package.name, grammar_path
            );
        }
        process_json_file(
            &grammar_path,
            &out_dir,
            &lang_key,
            "GRAMMAR",
            "grammar",
            |json| {
                plotnik_core::grammar::Grammar::from_json(json)
                    .expect("failed to parse grammar.json")
            },
        );

        // Process node-types.json
        let node_types_path = package_root.join("grammar/src/node-types.json");
        if node_types_path.exists() {
            process_json_file(
                &node_types_path,
                &out_dir,
                &lang_key,
                "NODE_TYPES",
                "node_types",
                |json| {
                    serde_json::from_str::<Vec<plotnik_core::RawNode>>(json)
                        .expect("failed to parse node-types.json")
                },
            );
        } else {
            panic!(
                "node-types.json not found for {}: {}",
                package.name, node_types_path
            );
        }
    }

    for (key, _) in std::env::vars() {
        if key.starts_with("CARGO_FEATURE_LANG_") {
            println!("cargo::rerun-if-env-changed={}", key);
        }
    }

    println!("cargo::rerun-if-changed=build.rs");
    println!("cargo::rerun-if-changed=Cargo.toml");
}

/// Parse JSON, serialize to binary, write to OUT_DIR, and set env var.
fn process_json_file<T, P, F>(
    json_path: P,
    out_dir: &Path,
    lang_key: &str,
    env_prefix: &str,
    file_suffix: &str,
    parse: F,
) where
    T: Serialize,
    P: AsRef<Path>,
    F: FnOnce(&str) -> T,
{
    let json_path = json_path.as_ref();
    let json = std::fs::read_to_string(json_path)
        .unwrap_or_else(|e| panic!("failed to read {}: {}", json_path.display(), e));

    let parsed = parse(&json);

    let binary = postcard::to_allocvec(&parsed).expect("serialization should not fail");
    let binary_path = out_dir.join(format!("{}.{}", lang_key.to_lowercase(), file_suffix));
    std::fs::write(&binary_path, &binary)
        .unwrap_or_else(|e| panic!("failed to write {}: {}", binary_path.display(), e));

    println!(
        "cargo::rustc-env=PLOTNIK_{}_{}={}",
        env_prefix,
        lang_key,
        binary_path.display()
    );
    println!("cargo::rerun-if-changed={}", json_path.display());
}

fn feature_to_lang_key(feature: &str) -> String {
    match feature {
        "lang-c-sharp" => "C_SHARP".to_string(),
        "lang-ssh-config" => "SSH_CONFIG".to_string(),
        _ => feature
            .strip_prefix("lang-")
            .unwrap_or(feature)
            .to_uppercase()
            .replace('-', "_"),
    }
}

fn arborium_package_to_feature(package_name: &str) -> Option<String> {
    const NON_LANGUAGE_PACKAGES: &[&str] = &[
        "arborium-docsrs-demo",
        "arborium-highlight",
        "arborium-host",
        "arborium-mdbook",
        "arborium-plugin-runtime",
        "arborium-rustdoc",
        "arborium-sysroot",
        "arborium-test-harness",
        "arborium-theme",
        "arborium-tree-sitter",
        "arborium-wire",
    ];

    if NON_LANGUAGE_PACKAGES.contains(&package_name) {
        return None;
    }

    package_name
        .strip_prefix("arborium-")
        .map(|suffix| format!("lang-{suffix}"))
}