use std::borrow::Cow;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::Once;
use crate::godot_version::parse_godot_version;
use crate::watch::StopWatch;
use crate::{GodotVersion, env_var_or_deprecated};
pub fn load_extension_api_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 load_gdextension_interface_json(watch: &mut StopWatch) -> Cow<'static, str> {
let godot_bin = locate_godot_binary();
rerun_on_changed(&godot_bin);
watch.record("locate_godot");
if !read_godot_version(&godot_bin).is_newer_than_latest() {
watch.record("load_prebuilt_header_json");
return gdextension_api::load_gdextension_interface_json();
}
let out_dir = std::env::var("OUT_DIR").unwrap();
let json_path = format!("{out_dir}/gdextension_interface.json");
dump_header_json_file(&godot_bin, &json_path);
watch.record("dump_header_json");
let contents = fs::read_to_string(json_path)
.expect("failed to load freshly created gdextension_interface.json");
Cow::Owned(contents)
}
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_or_panic();
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());
}
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 dump_header_json_file<P: AsRef<Path>>(godot_bin: &PathBuf, path: &P) {
let cwd = path.as_ref().parent().unwrap();
fs::create_dir_all(cwd)
.unwrap_or_else(|_| panic!("failed to create directory '{}'", cwd.display()));
let mut cmd = Command::new(godot_bin);
cmd.current_dir(cwd)
.arg("--headless")
.arg("--dump-gdextension-interface-json");
execute(cmd, "dump Godot header file");
}
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());
}