gdnative-bindings 0.11.3

The Godot game engine's automatcally generated bindings to Godot classes.
Documentation
use gdnative_bindings_generator as gen;

use std::fs::File;
use std::io::{BufWriter, Write as _};
use std::path::{Path, PathBuf};

fn main() {
    let just_generated_api = gen::generate_json_if_needed();

    let api_data = std::fs::read_to_string("api.json").expect("Unable to read api.json");

    let out_path = PathBuf::from(std::env::var("OUT_DIR").unwrap());

    let generated_rs = out_path.join("generated.rs");
    let icalls_rs = out_path.join("icalls.rs");

    let api = gen::Api::new(&api_data);
    let docs = gen::GodotXmlDocs::new("docs");
    let binding_res = gen::generate_bindings(&api, Some(&docs));

    {
        let mut output = BufWriter::new(File::create(&generated_rs).unwrap());

        generate(&out_path, &mut output, &binding_res);
    }

    {
        let mut output = BufWriter::new(File::create(&icalls_rs).unwrap());

        write!(output, "{}", binding_res.icalls).unwrap();
    }

    format_file_if_needed(&generated_rs);
    format_file_if_needed(&icalls_rs);

    // build.rs will automatically be recompiled and run if its dependencies are updated.
    // Ignoring everything but build.rs will avoid needless rebuilds.
    // Manually rebuilding the crate does not affect this.
    println!("cargo:rerun-if-changed=build.rs");

    // Avoid endless recompiling, if this script generates api.json and docs
    if !just_generated_api {
        println!("cargo:rerun-if-changed=docs/");
        println!("cargo:rerun-if-changed=api.json");
    }
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Feature 'one-class-one-file'

/// Output all the class bindings into the `generated.rs` file.
#[cfg(not(feature = "one-class-one-file"))]
fn generate(
    _out_path: &std::path::Path,
    generated_file: &mut BufWriter<File>,
    binding_res: &gen::BindingResult,
) {
    // Note: 'use super::*;' needs to be after content, as the latter may contain #![doc] attributes,
    // which need to be at the beginning of the module
    for (class, code) in &binding_res.class_bindings {
        let modifier = if class.has_related_module() {
            "pub"
        } else {
            "pub(crate)"
        };
        write!(
            generated_file,
            r#"
            {modifier} mod {mod_name} {{
                {content}
                use super::*;
            }}
            pub use crate::generated::{mod_name}::private::{class_name};
            "#,
            modifier = modifier,
            mod_name = gen::module_name_from_class_name(&class.name),
            class_name = class.name,
            content = code,
        )
        .unwrap();
    }
}

/// Output one file for each class and add `mod` and `use` declarations in
/// the `generated.rs` file.
#[cfg(feature = "one-class-one-file")]
fn generate(
    out_path: &std::path::Path,
    generated_file: &mut BufWriter<File>,
    binding_res: &gen::BindingResult,
) {
    for (class, code) in &binding_res.class_bindings {
        let mod_name = gen::module_name_from_class_name(&class.name);

        let mod_path = out_path.join(format!("{mod_name}.rs"));
        let mut mod_output = BufWriter::new(File::create(&mod_path).unwrap());

        write!(
            &mut mod_output,
            r#"
            {code}
            use super::*;
            "#,
        )
        .unwrap();

        drop(mod_output);

        format_file_if_needed(&mod_path);

        let modifier = if class.has_related_module() {
            "pub"
        } else {
            "pub(crate)"
        };
        writeln!(
            generated_file,
            r#"
            #[path = {:?}]
            {modifier} mod {mod_name};
            pub use crate::generated::{mod_name}::private::{class_name};
            "#,
            mod_path.display(),
            modifier = modifier,
            mod_name = mod_name,
            class_name = class.name,
        )
        .unwrap();
    }
}

// ----------------------------------------------------------------------------------------------------------------------------------------------
// Feature 'formatted'

#[cfg(feature = "formatted")]
fn format_file_if_needed(output_rs: &Path) {
    print!(
        "Formatting generated file: {}... ",
        output_rs.file_name().and_then(|s| s.to_str()).unwrap()
    );

    let output = std::process::Command::new("rustup")
        .arg("run")
        .arg("stable")
        .arg("rustfmt")
        .arg("--edition=2021")
        .arg(output_rs)
        .output();

    match output {
        Ok(_) => println!("Done."),
        Err(err) => {
            println!("Failed.");
            println!("Error: {err}");
        }
    }
}

#[cfg(not(feature = "formatted"))]
fn format_file_if_needed(_output_rs: &Path) {}