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 b9ac0ac510e15321b1659fe8ef275b08194c40e6 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 in build.rs, use workaround for generate bug if Godot < 3.3.1 --- gdnative-bindings/Cargo.toml | 1 + gdnative-bindings/build.rs | 82 +++++++++++++++++++------- gdnative-bindings/src/godot_version.rs | 66 +++++++++++++++++++++ 3 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 gdnative-bindings/src/godot_version.rs diff --git a/gdnative-bindings/Cargo.toml b/gdnative-bindings/Cargo.toml index 9d9f5a862..19808428f 100644 --- a/gdnative-bindings/Cargo.toml +++ b/gdnative-bindings/Cargo.toml @@ -25,3 +25,4 @@ bitflags = "1.3.2" heck = "0.4.0" gdnative_bindings_generator = { path = "../bindings_generator", version = "=0.9.3" } which = { optional = true, version = "4.2.2" } +regex = "1.5.4" diff --git a/gdnative-bindings/build.rs b/gdnative-bindings/build.rs index 8aec41da2..10f0facd3 100644 --- a/gdnative-bindings/build.rs +++ b/gdnative-bindings/build.rs @@ -154,17 +154,19 @@ fn format_file(output_rs: &Path) { } } -#[cfg(feature = "custom-godot")] +#[cfg(not(feature = "custom-godot"))] fn generate_api_if_needed() -> bool { - let source: String; - let godot_bin: PathBuf; + false +} - if let Ok(string) = env::var("GODOT_BIN") { - source = format!("GODOT_BIN executable '{}'", string); - godot_bin = PathBuf::from(string); +#[cfg(feature = "custom-godot")] +fn generate_api_if_needed() -> bool { + let godot_bin: PathBuf = if let Ok(string) = env::var("GODOT_BIN") { + println!("Found GODOT_BIN with path to executable: '{}'", string); + PathBuf::from(string) } else if let Ok(path) = which::which("godot") { - source = "executable 'godot'".to_string(); - godot_bin = path; + println!("Found 'godot' executable in PATH: {}", path.display()); + path } else { panic!( "Feature 'custom-godot' requires an accessible 'godot' executable or \ @@ -172,24 +174,62 @@ fn generate_api_if_needed() -> bool { ); }; - // TODO call 'godot --version' and ensure >= 3.2 && < 4.0 + let version = exec(1, Command::new(&godot_bin).arg("--version")); + + let has_generate_bug = match godot_version::parse_version(&version) { + Ok(parsed) => { + assert!( + parsed.major == 3 && parsed.minor >= 2, + "Only Godot versions >= 3.2 and < 4.0 are supported." + ); + + // 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 + } + }; - 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)); + // 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..."); - assert!( - status.success(), - "Custom Godot command exited with status {}", - status.code().map_or("?".to_string(), |f| f.to_string()) + exec( + if has_generate_bug { 10 } else { 1 }, + Command::new(&godot_bin) + .arg("--gdnative-generate-json-api") + .arg("api.json"), ); true } -#[cfg(not(feature = "custom-godot"))] -fn generate_api_if_needed() -> bool { - false +/// Executes a command and returns stdout. Panics on failure. +#[cfg(feature = "custom-godot")] +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).") +} + +#[cfg(feature = "custom-godot")] +#[allow(unused_variables, dead_code)] +mod godot_version { + // Not very nice, but there's no idiomatic approach to split build.rs, and #[path] freezes build.rs + include!("src/godot_version.rs"); } diff --git a/gdnative-bindings/src/godot_version.rs b/gdnative-bindings/src/godot_version.rs new file mode 100644 index 000000000..6da63f78f --- /dev/null +++ b/gdnative-bindings/src/godot_version.rs @@ -0,0 +1,66 @@ +use regex::Regex; +use std::error::Error; + +pub(crate) struct GodotVersion { + pub major: u8, + pub minor: u8, + pub patch: u8, //< 0 if none + pub stability: String, // stable|beta|dev +} + +pub(crate) fn parse_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 + ]; + + for (full, major, minor, patch, stability) in good_versions { + let parsed: GodotVersion = parse_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 { + let parsed = parse_version(full); + assert!(parsed.is_err()); + } +}