gdnative_project_utils/
generate.rs

1use path_slash::PathExt;
2use std::path::{Path, PathBuf};
3
4/// Build mode of the crate
5#[derive(Copy, Clone, Debug)]
6pub enum BuildMode {
7    Debug,
8    Release,
9}
10
11/// The filetype of the GDNativeLibrary
12#[derive(Copy, Clone, Debug)]
13pub enum LibFormat {
14    Gdnlib,
15    Tres,
16}
17
18/// A builder type that holds all necessary information about the project to
19/// generate files in all the right places.
20#[derive(Default)]
21pub struct Builder {
22    godot_project_dir: Option<PathBuf>,
23    godot_resource_output_dir: Option<PathBuf>,
24    target_dir: Option<PathBuf>,
25    lib_name: Option<String>,
26    build_mode: Option<BuildMode>,
27    lib_format: Option<LibFormat>,
28}
29
30impl Builder {
31    /// Construct a new Builder.
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// **REQUIRED** Set the path to the root of the Godot project.
37    pub fn with_godot_project_dir(&mut self, dir: impl AsRef<Path>) {
38        let dir = dir.as_ref().to_path_buf();
39
40        self.godot_project_dir = Some(dir);
41    }
42
43    /// **REQUIRED** Set the path to the root of the Godot project.
44    pub fn godot_project_dir(mut self, dir: impl AsRef<Path>) -> Self {
45        self.with_godot_project_dir(dir);
46        self
47    }
48
49    /// Set the path to the directory inside the Godot project to which the
50    /// generates files should be saved.
51    pub fn with_godot_resource_output_dir(&mut self, dir: impl AsRef<Path>) {
52        let dir = dir.as_ref().to_path_buf();
53
54        self.godot_resource_output_dir = Some(dir);
55    }
56
57    /// Set the path to the directory inside the Godot project to which the
58    /// generates files should be saved.
59    pub fn godot_resource_output_dir(mut self, dir: impl AsRef<Path>) -> Self {
60        self.with_godot_resource_output_dir(dir);
61        self
62    }
63
64    /// Set the path to the `target` directory in which cargo creates build
65    /// artefacts.
66    pub fn with_target_dir(&mut self, dir: impl AsRef<Path>) {
67        let dir = dir.as_ref().to_path_buf();
68
69        self.target_dir = Some(dir);
70    }
71
72    /// Set the path to the `target` directory in which cargo creates build
73    /// artefacts.
74    pub fn target_dir(mut self, dir: impl AsRef<Path>) -> Self {
75        self.with_target_dir(dir);
76        self
77    }
78
79    /// Set the type of the GDNativeLibrary Format
80    pub fn with_lib_format(&mut self, lib_format: LibFormat) {
81        self.lib_format = Some(lib_format);
82    }
83
84    /// Set the type of the GDNativeLibrary Format
85    pub fn lib_format(mut self, lib_format: LibFormat) -> Self {
86        self.with_lib_format(lib_format);
87        self
88    }
89
90    /// Set the name of the crate.
91    pub fn with_lib_name(&mut self, name: impl AsRef<str>) {
92        let name = name.as_ref().to_string();
93
94        self.lib_name = Some(name);
95    }
96
97    /// Set the name of the crate.
98    pub fn lib_name(mut self, name: impl AsRef<str>) -> Self {
99        self.with_lib_name(name);
100        self
101    }
102
103    /// Set the build mode of the crate.
104    ///
105    /// This will affect the path the `gdnlib` resource points to.
106    pub fn with_build_mode(&mut self, mode: BuildMode) {
107        self.build_mode = Some(mode);
108    }
109
110    /// Set the build mode of the crate.
111    ///
112    /// This will affect the path the `gdnlib` resource points to.
113    pub fn build_mode(mut self, mode: BuildMode) -> Self {
114        self.with_build_mode(mode);
115        self
116    }
117
118    /// Build and generate files for the crate and all `classes`.
119    ///
120    /// # Panics
121    ///
122    /// This function panics if the `godot_project_dir` has not been set.
123    pub fn build(self, classes: crate::scan::Classes) -> Result<(), std::io::Error> {
124        let lib_name = self
125            .lib_name
126            .or_else(|| std::env::var("CARGO_PKG_NAME").ok())
127            .expect("Package name not given and unable to find");
128        let godot_project_dir = self
129            .godot_project_dir
130            .and_then(|path| dunce::canonicalize(path).ok())
131            .expect("Godot project dir not given");
132        let godot_resource_output_dir = self
133            .godot_resource_output_dir
134            .and_then(|path| dunce::canonicalize(path).ok())
135            .unwrap_or_else(|| godot_project_dir.join("native"));
136        let target_dir = self
137            .target_dir
138            .and_then(|path| dunce::canonicalize(path).ok())
139            .or_else(|| {
140                let dir = std::env::var("CARGO_TARGET_DIR").ok()?;
141                dunce::canonicalize(PathBuf::from(dir)).ok()
142            })
143            .or_else(|| {
144                let dir = std::env::var("OUT_DIR").ok()?;
145                let out_path = PathBuf::from(&dir);
146
147                // target/{debug/release}/build/{crate}/out
148                dunce::canonicalize(out_path.join("../../../../")).ok()
149            })
150            .expect("Target dir not given and unable to find");
151        let build_mode = self
152            .build_mode
153            .or_else(|| {
154                let profile = std::env::var("PROFILE").ok()?;
155                match profile.as_str() {
156                    "release" => Some(BuildMode::Release),
157                    "debug" => Some(BuildMode::Debug),
158                    _ => None,
159                }
160            })
161            .expect("Build mode not given and unable to find");
162
163        std::fs::create_dir_all(&godot_resource_output_dir)?;
164
165        let lib_ext = match self.lib_format {
166            Some(LibFormat::Gdnlib) | None => "gdnlib",
167            Some(LibFormat::Tres) => "tres",
168        };
169        let gdnlib_path = godot_resource_output_dir.join(format!("{}.{}", lib_name, lib_ext));
170
171        {
172            let target_base_path = target_dir;
173
174            let target_rel_path = pathdiff::diff_paths(&target_base_path, &godot_project_dir)
175                .expect("Unable to create relative path between Godot project and library output");
176
177            let prefix;
178            let output_path;
179
180            if target_rel_path.starts_with("../") {
181                // not in the project folder, use an absolute path
182                prefix = "";
183                output_path = target_base_path;
184            } else {
185                // output paths are inside the project folder, use a `res://` path
186                prefix = "res://";
187                output_path = target_rel_path;
188            };
189
190            let binaries = common_binary_outputs(&output_path, build_mode, &lib_name);
191
192            let file_exists = gdnlib_path.exists() && gdnlib_path.is_file();
193
194            if !file_exists {
195                let content = match self.lib_format {
196                    Some(LibFormat::Gdnlib) | None => generate_gdnlib(prefix, binaries),
197                    Some(LibFormat::Tres) => generate_tres(prefix, binaries),
198                };
199                std::fs::write(&gdnlib_path, content)?;
200            }
201        }
202
203        let rel_gdnlib_path = pathdiff::diff_paths(&gdnlib_path, &godot_project_dir)
204            .expect("Unable to create relative path between Godot project and library output");
205
206        let prefix;
207        let output_path;
208
209        if rel_gdnlib_path.starts_with("../") {
210            // not in the project folder, use an absolute path
211            prefix = "";
212            output_path = &gdnlib_path;
213        } else {
214            // output paths are inside the project folder, use a `res://` path
215            prefix = "res://";
216            output_path = &rel_gdnlib_path;
217        };
218
219        for name in classes {
220            let path = godot_resource_output_dir.join(format!("{}.gdns", &name));
221
222            let file_exists = path.exists() && path.is_file();
223
224            if !file_exists {
225                let content = generate_gdns(&prefix, &output_path, &name);
226                std::fs::write(&path, content)?;
227            }
228        }
229
230        Ok(())
231    }
232}
233
234struct Binaries {
235    x11: PathBuf,
236    osx: PathBuf,
237    // TODO
238    // ios: PathBuf,
239    windows: PathBuf,
240    android_aarch64: PathBuf,
241    android_armv7: PathBuf,
242    android_x86: PathBuf,
243    android_x86_64: PathBuf,
244}
245
246fn common_binary_outputs(target: &Path, mode: BuildMode, name: &str) -> Binaries {
247    let mode_path = match mode {
248        BuildMode::Debug => "debug",
249        BuildMode::Release => "release",
250    };
251
252    // NOTE: If a crate has a hyphen in the name, at least on Linux the resulting library
253    // will have it replaced with an underscore. I assume other platforms do the same?
254    let name = name.replace("-", "_");
255
256    Binaries {
257        x11: target.join(mode_path).join(format!("lib{}.so", name)),
258        osx: target.join(mode_path).join(format!("lib{}.dylib", name)),
259
260        windows: target.join(mode_path).join(format!("{}.dll", name)),
261        android_armv7: target
262            .join("armv7-linux-androideabi")
263            .join(mode_path)
264            .join(format!("lib{}.so", name)),
265        android_aarch64: target
266            .join("aarch64-linux-android")
267            .join(mode_path)
268            .join(format!("lib{}.so", name)),
269        android_x86: target
270            .join("i686-linux-android")
271            .join(mode_path)
272            .join(format!("lib{}.so", name)),
273        android_x86_64: target
274            .join("x86_64-linux-android")
275            .join(mode_path)
276            .join(format!("lib{}.so", name)),
277    }
278}
279
280fn generate_tres(path_prefix: &str, binaries: Binaries) -> String {
281    format!(
282        r#"[gd_resource type="GDNativeLibrary" format=2]
283
284[resource]
285entry/Android.armeabi-v7a="{prefix}{android_armv7}"
286entry/Android.arm64-v8a="{prefix}{android_aarch64}"
287entry/Android.x86="{prefix}{android_x86}"
288entry/Android.x86_64="{prefix}{android_x86_64}"
289entry/X11.64="{prefix}{x11}"
290entry/OSX.64="{prefix}{osx}"
291entry/Windows.64="{prefix}{win}"
292dependency/Android.armeabi-v7a=[  ]
293dependency/Android.arm64-v8a=[  ]
294dependency/Android.x86=[  ]
295dependency/Android.x86_64=[  ]
296dependency/X11.64=[  ]
297dependency/OSX.64=[  ]
298"#,
299        prefix = path_prefix,
300        android_armv7 = binaries.android_armv7.to_slash_lossy(),
301        android_aarch64 = binaries.android_aarch64.to_slash_lossy(),
302        android_x86 = binaries.android_x86.to_slash_lossy(),
303        android_x86_64 = binaries.android_x86_64.to_slash_lossy(),
304        x11 = binaries.x11.to_slash_lossy(),
305        osx = binaries.osx.to_slash_lossy(),
306        win = binaries.windows.to_slash_lossy(),
307    )
308}
309
310fn generate_gdnlib(path_prefix: &str, binaries: Binaries) -> String {
311    format!(
312        r#"[entry]
313Android.armeabi-v7a="{prefix}{android_armv7}"
314Android.arm64-v8a="{prefix}{android_aarch64}"
315Android.x86="{prefix}{android_x86}"
316Android.x86_64="{prefix}{android_x86_64}"
317X11.64="{prefix}{x11}"
318OSX.64="{prefix}{osx}"
319Windows.64="{prefix}{win}"
320
321[dependencies]
322
323Android.armeabi-v7a=[  ]
324Android.arm64-v8a=[  ]
325Android.x86=[  ]
326Android.x86_64=[  ]
327X11.64=[  ]
328OSX.64=[  ]
329
330[general]
331
332singleton=false
333load_once=true
334symbol_prefix="godot_"
335reloadable=true"#,
336        prefix = path_prefix,
337        android_armv7 = binaries.android_armv7.to_slash_lossy(),
338        android_aarch64 = binaries.android_aarch64.to_slash_lossy(),
339        android_x86 = binaries.android_x86.to_slash_lossy(),
340        android_x86_64 = binaries.android_x86_64.to_slash_lossy(),
341        x11 = binaries.x11.to_slash_lossy(),
342        osx = binaries.osx.to_slash_lossy(),
343        win = binaries.windows.to_slash_lossy(),
344    )
345}
346
347fn generate_gdns(path_prefix: &str, gdnlib_path: &Path, name: &str) -> String {
348    format!(
349        r#"[gd_resource type="NativeScript" load_steps=2 format=2]
350
351[ext_resource path="{prefix}{gdnlib}" type="GDNativeLibrary" id=1]
352
353[resource]
354class_name = "{name}"
355script_class_name = "{name}"
356library = ExtResource( 1 )
357"#,
358        prefix = path_prefix,
359        gdnlib = gdnlib_path.to_slash_lossy(),
360        name = name,
361    )
362}