use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::Once;
use regex::Regex;
use crate::godot_version::{parse_godot_version, validate_godot_version};
use crate::header_gen::generate_rust_binding;
use crate::watch::StopWatch;
use crate::{GodotVersion, env_var_or_deprecated};
pub fn load_gdextension_json(watch: &mut StopWatch) -> String {
let path = format!("{}/extension_api.json", std::env::var("OUT_DIR").unwrap());
let json_path = Path::new(&path);
let godot_bin = locate_godot_binary();
rerun_on_changed(&godot_bin);
watch.record("locate_godot");
let _version = read_godot_version(&godot_bin);
dump_extension_api(&godot_bin, json_path);
watch.record("dump_api_json");
let result = fs::read_to_string(json_path)
.unwrap_or_else(|_| panic!("failed to open file {}", json_path.display()));
watch.record("read_api_json");
result
}
pub fn write_gdextension_headers(
inout_h_path: &Path,
out_rs_path: &Path,
is_h_provided: bool,
watch: &mut StopWatch,
) {
if !is_h_provided {
let godot_bin = locate_godot_binary();
rerun_on_changed(&godot_bin);
watch.record("locate_godot");
let _version = read_godot_version(&godot_bin);
dump_header_file(&godot_bin, inout_h_path);
watch.record("dump_header_h");
};
patch_c_header(inout_h_path, inout_h_path);
watch.record("patch_header_h");
generate_rust_binding(inout_h_path, out_rs_path);
watch.record("generate_header_rs");
}
pub(crate) fn read_godot_version(godot_bin: &Path) -> GodotVersion {
let mut cmd = Command::new(godot_bin);
cmd.arg("--version");
let output = execute(cmd, "read Godot version");
let stdout = std::str::from_utf8(&output.stdout).expect("convert Godot version to UTF-8");
let version = parse_godot_version(stdout)
.unwrap_or_else(|err| panic!("failed to parse Godot version '{stdout}': {err}"));
validate_godot_version(&version);
if !is_godot_debug_build(godot_bin) {
panic!(
"`api-custom` needs a Godot debug build (editor or debug export template); detected release build"
);
}
version
}
fn is_godot_debug_build(godot_bin: &Path) -> bool {
let mut cmd = Command::new(godot_bin);
cmd.arg("--help");
let haystack = execute(cmd, "Godot CLI help to check debug/release");
let needle = b"--dump-extension-api";
haystack
.stdout
.windows(needle.len())
.any(|window| window == needle)
}
fn dump_extension_api(godot_bin: &Path, out_file: &Path) {
let cwd = out_file.parent().unwrap();
fs::create_dir_all(cwd).unwrap_or_else(|_| panic!("create directory '{}'", cwd.display()));
println!("Dump GDExtension API JSON to dir '{}'...", cwd.display());
let mut cmd = Command::new(godot_bin);
cmd.current_dir(cwd)
.arg("--headless")
.arg("--dump-extension-api-with-docs");
execute(cmd, "dump Godot JSON file");
println!("Generated {}/extension_api.json.", cwd.display());
}
fn dump_header_file(godot_bin: &Path, out_file: &Path) {
let cwd = out_file.parent().unwrap();
fs::create_dir_all(cwd).unwrap_or_else(|_| panic!("create directory '{}'", cwd.display()));
println!("Dump GDExtension header file to dir '{}'...", cwd.display());
let mut cmd = Command::new(godot_bin);
cmd.current_dir(cwd)
.arg("--headless")
.arg("--dump-gdextension-interface");
execute(cmd, "dump Godot header file");
println!("Generated {}/gdextension_interface.h.", cwd.display());
}
pub(crate) fn patch_c_header(in_h_path: &Path, out_h_path: &Path) {
println!("Patch C header '{}'...", in_h_path.display());
let mut c = fs::read_to_string(in_h_path)
.unwrap_or_else(|_| panic!("failed to read C header file {}", in_h_path.display()));
assert!(
c.contains("GDExtensionInterfaceGetProcAddress"),
"C header file '{}' seems to be GDExtension version 4.0, which is no longer support by godot-rust.",
in_h_path.display()
);
c = c.replace(
"typedef void (*GDExtensionVariantFromTypeConstructorFunc)(GDExtensionVariantPtr, GDExtensionTypePtr);",
"typedef void (*GDExtensionVariantFromTypeConstructorFunc)(GDExtensionUninitializedVariantPtr, GDExtensionTypePtr);"
)
.replace(
"typedef void (*GDExtensionTypeFromVariantConstructorFunc)(GDExtensionTypePtr, GDExtensionVariantPtr);",
"typedef void (*GDExtensionTypeFromVariantConstructorFunc)(GDExtensionUninitializedTypePtr, GDExtensionVariantPtr);"
)
.replace(
"typedef void (*GDExtensionPtrConstructor)(GDExtensionTypePtr p_base, const GDExtensionConstTypePtr *p_args);",
"typedef void (*GDExtensionPtrConstructor)(GDExtensionUninitializedTypePtr p_base, const GDExtensionConstTypePtr *p_args);"
);
let c = Regex::new(r"typedef (const )?void \*GDExtension(Const)?([a-zA-Z0-9]+?)Ptr;") .expect("regex for mut typedef")
.replace_all(&c, "typedef ${1}struct __Gdext$3 *GDExtension${2}${3}Ptr;");
fs::write(out_h_path, c.as_ref()).unwrap_or_else(|_| {
panic!(
"failed to write patched C header file {}",
out_h_path.display()
)
});
}
pub(crate) fn locate_godot_binary() -> PathBuf {
static WARN_ONCE: Once = Once::new();
let env_var = env_var_or_deprecated(&WARN_ONCE, "GDRUST_GODOT_BIN", "GODOT4_BIN");
println!("cargo:rerun-if-env-changed=GDRUST_GODOT_BIN");
println!("cargo:rerun-if-env-changed=GODOT4_BIN");
if let Ok(string) = env_var {
println!("Found GDRUST_GODOT_BIN with path to executable: '{string}'");
PathBuf::from(string)
} else if let Ok(path) = which::which("godot4") {
println!("Found 'godot4' executable in PATH: {}", path.display());
path
} else {
panic!(
"godot-rust with `api-custom` feature requires 'godot4' executable or a \
GDRUST_GODOT_BIN environment variable (with the path to the executable)."
)
}
}
fn execute(cmd: Command, error_message: &str) -> Output {
try_execute(cmd, error_message).unwrap_or_else(|e| panic!("{}", e))
}
fn try_execute(mut cmd: Command, error_message: &str) -> Result<Output, String> {
let output = cmd.output().map_err(|e| {
format!("failed to invoke command ({error_message})\n\tError: {e}\n\tCommand: {cmd:?}")
})?;
println!("[stdout] {}", std::str::from_utf8(&output.stdout).unwrap());
println!("[stderr] {}", std::str::from_utf8(&output.stderr).unwrap());
println!("[status] {}", output.status);
if output.status.success() {
Ok(output)
} else {
Err(format!(
"command returned error ({error_message})\n\t{cmd:?}"
))
}
}
fn rerun_on_changed(path: &Path) {
println!("cargo:rerun-if-changed={}", path.display());
}