Skip to content

Commit c59591c

Browse files
bors[bot]Bromeon
andauthored
Merge #838
838: Godot version check + workaround for API generation bug r=Bromeon a=Bromeon As mentioned in #833, Godot versions < 3.3.1 were subject to [a bug](godotengine/godot#48081) that caused non-deterministic crashes during the command: ``` godot --gdnative-generate-json-api api.json ``` For users working with affected Godot versions (e.g. 3.2), this makes the feature flag `custom-godot` annoying to use, since they can't rely on the API generation to succeed -- let alone use it for automation/CI. This PR works around that by retrying the command up to 10 times (magic number). I changed the minimum supported Godot version in our own CI again to 3.2, meaning that if it _doesn't_ work, we have to suffer, too 😬 Additionally, this PR parses the Godot version and emits a meaningful error message if an unsupported version is detected (3.1 or 4.0). Co-authored-by: Jan Haller <[email protected]>
2 parents 2d2476a + a10f6d6 commit c59591c

File tree

7 files changed

+202
-86
lines changed

7 files changed

+202
-86
lines changed

.github/workflows/full-ci.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,9 @@ jobs:
244244
godot: "3.4.1"
245245
postfix: ' (msrv 1.51)'
246246

247-
# Test with older engine version
248-
# Note: headless versions of Godot <= 3.3 may crash with a bug, see feature description in lib.rs
247+
# Test with oldest supported engine version
249248
- rust: stable
250-
godot: "3.3.1"
249+
godot: "3.2"
251250
postfix: ''
252251
build_args: '--features custom-godot'
253252

bindings_generator/Cargo.toml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ edition = "2018"
1212

1313
[features]
1414
debug = []
15+
custom-godot = ["which"]
1516

1617
[dependencies]
1718
heck = "0.4.0"
18-
roxmltree = "0.14.1"
19+
memchr = "2.4.1"
20+
miniserde = "0.1.15"
1921
proc-macro2 = "1.0.30"
2022
quote = "1.0.10"
23+
regex = "1.5.4"
24+
roxmltree = "0.14.1"
2125
syn = { version = "1.0.80", features = ["full", "extra-traits", "visit"] }
22-
miniserde = "0.1.15"
2326
unindent = "0.1.7"
24-
regex = "1.5.4"
25-
memchr = "2.4.1"
27+
which = { optional = true, version = "4.2.2" }
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
use crate::godot_version;
2+
use std::path::{Path, PathBuf};
3+
use std::process::Command;
4+
5+
pub fn generate_json_if_needed() -> bool {
6+
let godot_bin: PathBuf = if let Ok(string) = std::env::var("GODOT_BIN") {
7+
println!("Found GODOT_BIN with path to executable: '{}'", string);
8+
PathBuf::from(string)
9+
} else if let Ok(path) = which::which("godot") {
10+
println!("Found 'godot' executable in PATH: {}", path.display());
11+
path
12+
} else {
13+
panic!(
14+
"Feature 'custom-godot' requires an accessible 'godot' executable or \
15+
a GODOT_BIN environment variable (with the path to the executable)."
16+
);
17+
};
18+
19+
let version = exec(1, Command::new(&godot_bin).arg("--version"));
20+
21+
let has_generate_bug = match godot_version::parse_godot_version(&version) {
22+
Ok(parsed) => {
23+
assert!(
24+
parsed.major == 3 && parsed.minor >= 2,
25+
"Only Godot versions >= 3.2 and < 4.0 are supported; found version {}.",
26+
version
27+
);
28+
29+
// bug for versions < 3.3.1
30+
parsed.major == 2 || parsed.major == 3 && parsed.minor == 0
31+
}
32+
Err(e) => {
33+
// Don't treat this as fatal error
34+
eprintln!("Warning, failed to parse version: {}", e);
35+
true // version not known, conservatively assume bug
36+
}
37+
};
38+
39+
// Workaround for Godot bug, where the generate command crashes the engine.
40+
// Try 10 times (should be reasonably high confidence that at least 1 run succeeds).
41+
println!("Found Godot version < 3.3.1 with potential generate bug; trying multiple times...");
42+
43+
exec(
44+
if has_generate_bug { 10 } else { 1 },
45+
Command::new(&godot_bin)
46+
.arg("--gdnative-generate-json-api")
47+
.arg("api.json"),
48+
);
49+
50+
true
51+
}
52+
53+
/// Executes a command and returns stdout. Panics on failure.
54+
fn exec(attempts: i32, command: &mut Command) -> String {
55+
let command_line = format!("{:?}", command);
56+
57+
for _attempt in 0..attempts {
58+
match command.output() {
59+
Ok(output) => return String::from_utf8(output.stdout).expect("parse UTF8 string"),
60+
Err(err) => {
61+
eprintln!(
62+
"Godot command failed:\n command: {}\n error: {}",
63+
command_line, err
64+
)
65+
}
66+
}
67+
}
68+
69+
panic!("Could not execute Godot command (see above).")
70+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
//#![allow(unused_variables, dead_code)]
2+
3+
use regex::Regex;
4+
use std::error::Error;
5+
6+
pub struct GodotVersion {
7+
pub major: u8,
8+
pub minor: u8,
9+
pub patch: u8, //< 0 if none
10+
pub stability: String, // stable|beta|dev
11+
}
12+
13+
pub fn parse_godot_version(version_str: &str) -> Result<GodotVersion, Box<dyn Error>> {
14+
let regex = Regex::new("(\\d+)\\.(\\d+)(?:\\.(\\d+))?\\.(stable|beta|dev)")?;
15+
16+
let caps = regex.captures(version_str).ok_or("Regex capture failed")?;
17+
18+
let fail = || {
19+
format!(
20+
"Version substring could not be matched in '{}'",
21+
version_str
22+
)
23+
};
24+
25+
Ok(GodotVersion {
26+
major: caps.get(1).ok_or_else(fail)?.as_str().parse::<u8>()?,
27+
minor: caps.get(2).ok_or_else(fail)?.as_str().parse::<u8>()?,
28+
patch: caps
29+
.get(3)
30+
.map(|m| m.as_str().parse::<u8>())
31+
.transpose()?
32+
.unwrap_or(0),
33+
stability: caps.get(4).ok_or_else(fail)?.as_str().to_string(),
34+
})
35+
}
36+
37+
#[test]
38+
fn test_godot_versions() {
39+
let good_versions = [
40+
("3.0.stable.official", 3, 0, 0, "stable"),
41+
("3.0.1.stable.official", 3, 0, 1, "stable"),
42+
("3.2.stable.official", 3, 2, 0, "stable"),
43+
("3.37.stable.official", 3, 37, 0, "stable"),
44+
("3.4.stable.official.206ba70f4", 3, 4, 0, "stable"),
45+
("3.4.1.stable.official.aa1b95889", 3, 4, 1, "stable"),
46+
("3.5.beta.custom_build.837f2c5f8", 3, 5, 0, "beta"),
47+
("4.0.dev.custom_build.e7e9e663b", 4, 0, 0, "dev"),
48+
];
49+
50+
let bad_versions = [
51+
"4.0.unstable.custom_build.e7e9e663b", // "unstable"
52+
"4.0.3.custom_build.e7e9e663b", // no stability
53+
"3.stable.official.206ba70f4", // no minor
54+
];
55+
56+
for (full, major, minor, patch, stability) in good_versions {
57+
let parsed: GodotVersion = parse_godot_version(full).unwrap();
58+
assert_eq!(parsed.major, major);
59+
assert_eq!(parsed.minor, minor);
60+
assert_eq!(parsed.patch, patch);
61+
assert_eq!(parsed.stability, stability);
62+
}
63+
64+
for full in bad_versions {
65+
let parsed = parse_godot_version(full);
66+
assert!(parsed.is_err());
67+
}
68+
}

bindings_generator/src/lib.rs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,29 +13,42 @@
1313
//! must be taken to ensure that the version of the generator matches the one specified in
1414
//! the `Cargo.toml` of the `gdnative` crate exactly, even for updates that are considered
1515
//! non-breaking in the `gdnative` crate.
16-
use proc_macro2::TokenStream;
17-
18-
use quote::{format_ident, quote};
1916
20-
pub mod api;
2117
mod class_docs;
2218
mod classes;
23-
pub mod dependency;
2419
mod documentation;
2520
mod methods;
2621
mod special_methods;
2722

28-
pub use crate::api::*;
29-
pub use crate::class_docs::*;
23+
#[cfg(feature = "custom-godot")]
24+
mod godot_api_json;
25+
mod godot_version;
26+
27+
pub mod api;
28+
pub mod dependency;
29+
3030
use crate::classes::*;
31-
pub use crate::dependency::*;
3231
use crate::documentation::*;
3332
use crate::methods::*;
3433
use crate::special_methods::*;
35-
34+
use proc_macro2::TokenStream;
35+
use quote::{format_ident, quote};
3636
use std::collections::HashMap;
3737
use std::io;
3838

39+
pub use api::*;
40+
pub use class_docs::*;
41+
pub use dependency::*;
42+
43+
#[cfg(feature = "custom-godot")]
44+
pub use godot_api_json::*;
45+
pub use godot_version::*;
46+
47+
#[cfg(not(feature = "custom-godot"))]
48+
pub fn generate_json_if_needed() -> bool {
49+
false
50+
}
51+
3952
pub type GeneratorResult<T = ()> = Result<T, io::Error>;
4053

4154
pub struct BindingResult<'a> {

gdnative-bindings/Cargo.toml

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,13 @@ edition = "2018"
1313
[features]
1414
formatted = []
1515
one-class-one-file = []
16-
custom-godot = ["which"]
16+
custom-godot = ["gdnative_bindings_generator/custom-godot"]
1717

1818
[dependencies]
19-
gdnative-sys = { path = "../gdnative-sys", version = "0.9.3" }
20-
gdnative-core = { path = "../gdnative-core", version = "=0.9.3" }
19+
gdnative-sys = { path = "../gdnative-sys" }
20+
gdnative-core = { path = "../gdnative-core" }
2121
libc = "0.2.104"
2222
bitflags = "1.3.2"
2323

2424
[build-dependencies]
25-
heck = "0.4.0"
26-
gdnative_bindings_generator = { path = "../bindings_generator", version = "=0.9.3" }
27-
which = { optional = true, version = "4.2.2" }
25+
gdnative_bindings_generator = { path = "../bindings_generator" }

0 commit comments

Comments
 (0)