From 1d7655eabeef2f2e9ee6eca0297b466e09a11d49 Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 21 Dec 2021 22:49:47 +0100 Subject: [PATCH 1/2] CI: use oldest supported Godot version (3.2) --- .github/workflows/full-ci.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/full-ci.yml b/.github/workflows/full-ci.yml index 706dc2d09..0574daf9e 100644 --- a/.github/workflows/full-ci.yml +++ b/.github/workflows/full-ci.yml @@ -244,10 +244,9 @@ jobs: godot: "3.4.1" postfix: ' (msrv 1.51)' - # Test with older engine version - # Note: headless versions of Godot <= 3.3 may crash with a bug, see feature description in lib.rs + # Test with oldest supported engine version - rust: stable - godot: "3.3.1" + godot: "3.2" postfix: '' build_args: '--features custom-godot' From 8c7c84aef796e02c5dd1dd1562f2db96cd73829d Mon Sep 17 00:00:00 2001 From: Jan Haller Date: Tue, 21 Dec 2021 23:17:30 +0100 Subject: [PATCH 2/2] Parse Godot version; work around api-generate bug if Godot < 3.3.1 Move logic from build.rs to bindings_generator --- bindings_generator/Cargo.toml | 10 ++- bindings_generator/src/godot_api_json.rs | 70 ++++++++++++++++++ bindings_generator/src/godot_version.rs | 69 +++++++++++++++++ bindings_generator/src/lib.rs | 31 +++++--- gdnative-bindings/Cargo.toml | 10 +-- gdnative-bindings/build.rs | 94 ++++++++---------------- 6 files changed, 201 insertions(+), 83 deletions(-) create mode 100644 bindings_generator/src/godot_api_json.rs create mode 100644 bindings_generator/src/godot_version.rs diff --git a/bindings_generator/Cargo.toml b/bindings_generator/Cargo.toml index 1778a676d..b8ece6d68 100644 --- a/bindings_generator/Cargo.toml +++ b/bindings_generator/Cargo.toml @@ -12,14 +12,16 @@ edition = "2018" [features] debug = [] +custom-godot = ["which"] [dependencies] heck = "0.4.0" -roxmltree = "0.14.1" +memchr = "2.4.1" +miniserde = "0.1.15" proc-macro2 = "1.0.30" quote = "1.0.10" +regex = "1.5.4" +roxmltree = "0.14.1" syn = { version = "1.0.80", features = ["full", "extra-traits", "visit"] } -miniserde = "0.1.15" unindent = "0.1.7" -regex = "1.5.4" -memchr = "2.4.1" +which = { optional = true, version = "4.2.2" } diff --git a/bindings_generator/src/godot_api_json.rs b/bindings_generator/src/godot_api_json.rs new file mode 100644 index 000000000..84c236588 --- /dev/null +++ b/bindings_generator/src/godot_api_json.rs @@ -0,0 +1,70 @@ +use crate::godot_version; +use std::path::PathBuf; +use std::process::Command; + +pub fn generate_json_if_needed() -> bool { + let godot_bin: PathBuf = if let Ok(string) = std::env::var("GODOT_BIN") { + println!("Found GODOT_BIN with path to executable: '{}'", string); + PathBuf::from(string) + } else if let Ok(path) = which::which("godot") { + println!("Found 'godot' executable in PATH: {}", path.display()); + path + } else { + panic!( + "Feature 'custom-godot' requires an accessible 'godot' executable or \ + a GODOT_BIN environment variable (with the path to the executable)." + ); + }; + + let version = exec(1, Command::new(&godot_bin).arg("--version")); + + let has_generate_bug = match godot_version::parse_godot_version(&version) { + Ok(parsed) => { + assert!( + parsed.major == 3 && parsed.minor >= 2, + "Only Godot versions >= 3.2 and < 4.0 are supported; found version {}.", + version.trim() + ); + + // bug for versions < 3.3.1 + parsed.major == 2 || parsed.major == 3 && parsed.minor == 0 + } + Err(e) => { + // Don't treat this as fatal error + eprintln!("Warning, failed to parse version: {}", e); + true // version not known, conservatively assume bug + } + }; + + // Workaround for Godot bug, where the generate command crashes the engine. + // Try 10 times (should be reasonably high confidence that at least 1 run succeeds). + println!("Found Godot version < 3.3.1 with potential generate bug; trying multiple times..."); + + exec( + if has_generate_bug { 10 } else { 1 }, + Command::new(&godot_bin) + .arg("--gdnative-generate-json-api") + .arg("api.json"), + ); + + true +} + +/// Executes a command and returns stdout. Panics on failure. +fn exec(attempts: i32, command: &mut Command) -> String { + let command_line = format!("{:?}", command); + + for _attempt in 0..attempts { + match command.output() { + Ok(output) => return String::from_utf8(output.stdout).expect("parse UTF8 string"), + Err(err) => { + eprintln!( + "Godot command failed:\n command: {}\n error: {}", + command_line, err + ) + } + } + } + + panic!("Could not execute Godot command (see above).") +} diff --git a/bindings_generator/src/godot_version.rs b/bindings_generator/src/godot_version.rs new file mode 100644 index 000000000..3567748bd --- /dev/null +++ b/bindings_generator/src/godot_version.rs @@ -0,0 +1,69 @@ +//#![allow(unused_variables, dead_code)] + +use regex::Regex; +use std::error::Error; + +pub struct GodotVersion { + pub major: u8, + pub minor: u8, + pub patch: u8, //< 0 if none + pub stability: String, // stable|beta|dev +} + +pub fn parse_godot_version(version_str: &str) -> Result> { + let regex = Regex::new("(\\d+)\\.(\\d+)(?:\\.(\\d+))?\\.(stable|beta|dev)")?; + + let caps = regex.captures(version_str).ok_or("Regex capture failed")?; + + let fail = || { + format!( + "Version substring could not be matched in '{}'", + version_str + ) + }; + + Ok(GodotVersion { + major: caps.get(1).ok_or_else(fail)?.as_str().parse::()?, + minor: caps.get(2).ok_or_else(fail)?.as_str().parse::()?, + patch: caps + .get(3) + .map(|m| m.as_str().parse::()) + .transpose()? + .unwrap_or(0), + stability: caps.get(4).ok_or_else(fail)?.as_str().to_string(), + }) +} + +#[test] +fn test_godot_versions() { + let good_versions = [ + ("3.0.stable.official", 3, 0, 0, "stable"), + ("3.0.1.stable.official", 3, 0, 1, "stable"), + ("3.2.stable.official", 3, 2, 0, "stable"), + ("3.37.stable.official", 3, 37, 0, "stable"), + ("3.4.stable.official.206ba70f4", 3, 4, 0, "stable"), + ("3.4.1.stable.official.aa1b95889", 3, 4, 1, "stable"), + ("3.5.beta.custom_build.837f2c5f8", 3, 5, 0, "beta"), + ("4.0.dev.custom_build.e7e9e663b", 4, 0, 0, "dev"), + ]; + + let bad_versions = [ + "4.0.unstable.custom_build.e7e9e663b", // "unstable" + "4.0.3.custom_build.e7e9e663b", // no stability + "3.stable.official.206ba70f4", // no minor + ]; + + // From Rust 1.56: 'for (...) in good_versions' + for (full, major, minor, patch, stability) in good_versions.iter().cloned() { + let parsed: GodotVersion = parse_godot_version(full).unwrap(); + assert_eq!(parsed.major, major); + assert_eq!(parsed.minor, minor); + assert_eq!(parsed.patch, patch); + assert_eq!(parsed.stability, stability); + } + + for full in bad_versions.iter() { + let parsed = parse_godot_version(full); + assert!(parsed.is_err()); + } +} diff --git a/bindings_generator/src/lib.rs b/bindings_generator/src/lib.rs index e74a09f3c..d661638a9 100644 --- a/bindings_generator/src/lib.rs +++ b/bindings_generator/src/lib.rs @@ -13,29 +13,42 @@ //! must be taken to ensure that the version of the generator matches the one specified in //! the `Cargo.toml` of the `gdnative` crate exactly, even for updates that are considered //! non-breaking in the `gdnative` crate. -use proc_macro2::TokenStream; - -use quote::{format_ident, quote}; -pub mod api; mod class_docs; mod classes; -pub mod dependency; mod documentation; mod methods; mod special_methods; -pub use crate::api::*; -pub use crate::class_docs::*; +#[cfg(feature = "custom-godot")] +mod godot_api_json; +mod godot_version; + +pub mod api; +pub mod dependency; + use crate::classes::*; -pub use crate::dependency::*; use crate::documentation::*; use crate::methods::*; use crate::special_methods::*; - +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; use std::collections::HashMap; use std::io; +pub use api::*; +pub use class_docs::*; +pub use dependency::*; + +#[cfg(feature = "custom-godot")] +pub use godot_api_json::*; +pub use godot_version::*; + +#[cfg(not(feature = "custom-godot"))] +pub fn generate_json_if_needed() -> bool { + false +} + pub type GeneratorResult = Result; pub struct BindingResult<'a> { diff --git a/gdnative-bindings/Cargo.toml b/gdnative-bindings/Cargo.toml index 9d9f5a862..99400f6d3 100644 --- a/gdnative-bindings/Cargo.toml +++ b/gdnative-bindings/Cargo.toml @@ -13,15 +13,13 @@ edition = "2018" [features] formatted = [] one-class-one-file = [] -custom-godot = ["which"] +custom-godot = ["gdnative_bindings_generator/custom-godot"] [dependencies] -gdnative-sys = { path = "../gdnative-sys", version = "0.9.3" } -gdnative-core = { path = "../gdnative-core", version = "=0.9.3" } +gdnative-sys = { path = "../gdnative-sys" } +gdnative-core = { path = "../gdnative-core" } libc = "0.2.104" bitflags = "1.3.2" [build-dependencies] -heck = "0.4.0" -gdnative_bindings_generator = { path = "../bindings_generator", version = "=0.9.3" } -which = { optional = true, version = "4.2.2" } +gdnative_bindings_generator = { path = "../bindings_generator" } diff --git a/gdnative-bindings/build.rs b/gdnative-bindings/build.rs index 8aec41da2..4cb7293b8 100644 --- a/gdnative-bindings/build.rs +++ b/gdnative-bindings/build.rs @@ -1,24 +1,22 @@ -use gdnative_bindings_generator::*; +use gdnative_bindings_generator as gen; -use std::env; use std::fs::File; use std::io::{BufWriter, Write as _}; use std::path::{Path, PathBuf}; -use std::process::Command; fn main() { - let just_generated_api = generate_api_if_needed(); + 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(env::var("OUT_DIR").unwrap()); + 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 = Api::new(&api_data); - let docs = GodotXmlDocs::new("docs"); - let binding_res = generate_bindings(&api, Some(&docs)); + 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()); @@ -32,10 +30,8 @@ fn main() { write!(&mut output, "{}", binding_res.icalls).unwrap(); } - if cfg!(feature = "formatted") { - format_file(&generated_rs); - format_file(&icalls_rs); - } + format_file_if_needed(&generated_rs); + format_file_if_needed(&icalls_rs); // build.rs will automatically be recompiled and run if it's dependencies are updated. // Ignoring all but build.rs will keep from needless rebuilds. @@ -49,12 +45,15 @@ fn main() { } } +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// 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, - binding_res: &BindingResult, + 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 @@ -74,7 +73,7 @@ fn generate( pub use crate::generated::{mod_name}::private::{class_name}; "#, modifier = modifier, - mod_name = module_name_from_class_name(&class.name), + mod_name = gen::module_name_from_class_name(&class.name), class_name = class.name, content = code, ) @@ -88,10 +87,10 @@ fn generate( fn generate( out_path: &std::path::Path, generated_file: &mut BufWriter, - binding_res: &BindingResult, + binding_res: &gen::BindingResult, ) { for (class, code) in &binding_res.class_bindings { - let mod_name = module_name_from_class_name(&class.name); + let mod_name = gen::module_name_from_class_name(&class.name); let mod_path = out_path.join(format!("{}.rs", mod_name)); let mut mod_output = BufWriter::new(File::create(&mod_path).unwrap()); @@ -108,9 +107,7 @@ fn generate( drop(mod_output); - if cfg!(feature = "formatted") { - format_file(&mod_path); - } + format_file_if_needed(&mod_path); let modifier = if class.has_related_module() { "pub" @@ -133,63 +130,32 @@ fn generate( } } -fn format_file(output_rs: &Path) { +// ---------------------------------------------------------------------------------------------------------------------------------------------- +// 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() ); - match Command::new("rustup") + + let output = std::process::Command::new("rustup") .arg("run") .arg("stable") .arg("rustfmt") .arg("--edition=2018") .arg(output_rs) - .output() - { - Ok(_) => println!("Done"), + .output(); + + match output { + Ok(_) => println!("Done."), Err(err) => { - println!("Failed"); + println!("Failed."); println!("Error: {}", err); } } } -#[cfg(feature = "custom-godot")] -fn generate_api_if_needed() -> bool { - let source: String; - let godot_bin: PathBuf; - - if let Ok(string) = env::var("GODOT_BIN") { - source = format!("GODOT_BIN executable '{}'", string); - godot_bin = PathBuf::from(string); - } else if let Ok(path) = which::which("godot") { - source = "executable 'godot'".to_string(); - godot_bin = path; - } else { - panic!( - "Feature 'custom-godot' requires an accessible 'godot' executable or \ - a GODOT_BIN environment variable (with the path to the executable)." - ); - }; - - // TODO call 'godot --version' and ensure >= 3.2 && < 4.0 - - let status = Command::new(godot_bin) - .arg("--gdnative-generate-json-api") - .arg("api.json") - .status() - .unwrap_or_else(|err| panic!("Failed to invoke {}; error {}", source, err)); - - assert!( - status.success(), - "Custom Godot command exited with status {}", - status.code().map_or("?".to_string(), |f| f.to_string()) - ); - - true -} - -#[cfg(not(feature = "custom-godot"))] -fn generate_api_if_needed() -> bool { - false -} +#[cfg(not(feature = "formatted"))] +fn format_file_if_needed(_output_rs: &Path) {}