gdext_gen/
lib.rs

1//! # GDExtension loading file generation for Rust
2//!
3//! This crate aims to provide a way to autogenerate a `.gdextension` file for using `Rust` to make a `Godot` `GDExtension`. It provides all the libraries pathfinding and a way to automatically link the default icons to the new defined classes based on the class they inherit from, and also a way to manually link a specific class with a custom icon. For more information, read the [documentation](https://docs.rs/gdext-gen), or the [source code](https://github.com/sylbeth/gdext-generation).
4//!
5//! # Installation
6//!
7//! To install this crate as a build dependency in your own crate, run: `cargo add --build gdext-gen`. If you instead want it added as a normal dependency run: `cargo add gdext-gen`.
8//!
9//! # Usage
10//!
11//! ## build.rs call
12//!
13//! To get all the functionality of this crate, in your `build.rs` file on the root of your crate (not your `src/`), write the following (parameters may vary depending on the features you've opt in or out of):
14//!
15//! ```rust
16//! use gdext_gen::prelude::*;
17//! fn main() {
18//!     // All your variable initialization and setup goes here.
19//!     generate_gdextension_file(base_dir, target_dir, gdextension_path, force_generation, configuration, windows_abi, icons_configuration, dependencies);
20//! }
21//! ```
22//!
23//! The parameters of this function and how it works are documented in the docs. It should be kept in mind that this function returns an `std::io::Result`, so the following code could be used instead:
24//!
25//! ```rust
26//! use std::io::Result;
27//! use gdext_gen::prelude::*;
28//!
29//! fn main() -> Result<()> {
30//!     // All your variable initialization and setup goes here.
31//!     generate_gdextension_file(base_dir, target_dir, gdextension_path, force_generation, configuration, windows_abi, icons_configuration, dependencies)?;
32//! }
33//! ```
34//!
35//! To compile for `Android`, `Web`, `MacOS` or `iOS` please refer to the [`godot-rust` book](https://godot-rust.github.io/book/toolchain/index.html).
36//!
37//! It's worth noting that one can configure when the build script will be run, so it's sensible to change it were one not to need it running at every source file change.
38//!
39//! ## Variable initialization
40//!
41//! An example of variable initialization to have parity with the `godot-rust` example is the following (with all the primaty features enabled and `checked_generation` chosen):
42//!
43//! ```rust
44//! use std::io::Result;
45//! use gdext_gen::prelude::*;
46//! fn main() -> Result<()> {
47//!     generate_gdextension_file(
48//!         BaseDirectory::ProjectFolder.into(),
49//!         Some("../rust/target".into()),
50//!         Some("../godot/rust.gdextension".into()),
51//!         true,
52//!         Some(Configuration::new(
53//!             EntrySymbol::GodotRustDefault,
54//!             Some((4, 1)),
55//!             None,
56//!             true,
57//!             false,
58//!         )),
59//!         Some(WindowsABI::MSVC),
60//!         Some(IconsConfig::new(
61//!             DefaultNodeIcon::NodeRust(NodeRust::Ferris, "rust".into()),
62//!             IconsCopyStrategy::new(true, true, "../godot/addons/rust".into(), false),
63//!             None,
64//!             IconsDirectories::new("addons".into(), "editor".into(), "rust".into(), BaseDirectory::ProjectFolder.into()),
65//!         )),
66//!         None,
67//!     )?;
68//!
69//!     Ok(())
70//! }
71//! ```
72//! This results in a "rust.gdextension" file in "Project/godot", which contains the following:
73//! ```toml
74//! [configuration]
75//! entry_symbol = "gdext_rust_init"
76//! compatibility_minimum = 4.1
77//! reloadable = true
78//!
79//! [libraries]
80//! "target.mode" = "res://../rust/target/mode/library.file"
81//! "target.mode.architecture" = "res://../rust/target/target-triple/mode/library.file"
82//! ...
83//!
84//! [icons]
85//! YourStructName = "res://addons/rust/NodeRust.svg"
86//! ...
87//! ```
88//!
89//! Few lines of code for a customized automated `.gdextension` file, in conclusion.
90//!
91//! ## Variables short explanation
92//!
93//! Based on the last example, the GDExtension is configured as follows:
94//! - `BaseDirectory::ProjectFolder` uses `"res://"` based paths.
95//! - `target_dir = "../rust/target"`: The target folder for the GDExtension crate is found at `"res://../rust/target"`.
96//! - `gdextension_path = "../godot/rust.gdextension`: Makes the file at `"Project/godot/rust.gdextension"` (if `"rust"` and `"godot"` are in a `"Project"` folder).
97//! - `true` here means the `.gdextension` will be rewritten even if the file already exists.
98//! - `EntrySymbol::GodotRustDefault` defaults to `"gdext_rust_init"`.
99//! - `minimum_compatibility` -> 4.1 and `reloatable =  true`
100//! - `WindowsABI::MSVC` uses `MSVC` as linker and environment when compiling for `Windows`.
101//! - `DefaultNodeIcon::NodeRust(NodeRust::Ferris, "rust")` uses the `NodeRustFerris.svg` icon and finds it in the folder `"res://{base_dir}/rust"`.
102//! - IconsCopyStrategy: true, copy the `NodeRust` (and true) file**s** in path `"../godot/addons/rust"` relative to your crate and if it's there, don't copy it again.
103//! - No custom nodes.
104//! - The directories will be laid out as following:
105//!     - All icons will be found relative to `"res://addons"`.
106//!     - The editor icons will be located in `"res://addons/editor"`.
107//!     - The custom nodes will be located in `"res://addons/rust"`
108//! - None: No dependencies.
109//!
110//! # Features
111//!
112//! - `icons` - Allows the use of custom icons and the copying of `Rust`'s default icons for the generation of the `icons` section of the `.gdextension` file.
113//! - `find_icons` - Allows for the finding of the names of the custom implemented nodes and their subclasses using regex to automate the `icons` section generation process.
114//! - `dependencies` - Allows for the generation of the `dependencies` section of the `.gdextension` file.
115//! - `checked_generation` - Adds a parameter to the function call to allow for specifying whether the `.gdextension` file should always be copied or only when it doesn't exist. This option is mutually exclusive with `forced_generation`. If none is chosen, it defaults to writing it only when it doesn't exist.
116//! - `forced_generation` - Ensures the `.gdextension` file is always written regardless of whether it exists or not. This option is mutually exclusive with `checked_generation`. If none is chosen, it defaults to writing it only when it doesn't exist.
117//!
118//! # Limitations
119//!
120//! The feature "find_icons" uses regex to do its work. It's not a perfect way of finding the icons for each GDExtension custom node, but it always resets after each file, so one file's contents failing can only affect itself. It does so by searching for lines that contain both `"base"` and `"="`, then trying to find the name of the base. Same with `"struct"`. The only ways it could fail is if that exact appearance is in a comment or string, has comments in between or extends over more than a line. I believe these to be reasonable compromises, as searching for more than these would only make the code slower, and any reasonably formatted code would have `"base ="` in the same line and for `"base = NameBase"`, or struct `"NameStruct {"` to appear on their own in a comment is hard enough, and the auto found icons can ALWAYS be overriden by custom icons that just happen to be the editor's. In any case, if one thinks otherwise, here are other ways to implement this. 1: A pretty barebones Rust parser, 2: Preprocessing strings and comments in a file before doing the search, 3: Searching for the `impl INameOfBase for StructName`. If you experience problems due to this fact, due let us know, there may be a fix for it.
121//!
122//! There is also an issue with structs that use generics, or structs that don't follow the standard. These, may not be found at all, so it's best to just add them as custom.
123//!
124//! # Acknowledgements
125//!
126//! - This crate is based on the [`gdextension_file` documentation](https://docs.godotengine.org/en/stable/tutorials/scripting/gdextension/gdextension_file.html) from [`Godot`](https://godotengine.org/), and some snippets of the documentation (all written by [paddy-exe](https://github.com/paddy-exe)) are taken as are from their documentation, so they are as accurate as possible. The copyright notices for those files can be found directly in their [repository](https://github.com/godotengine/godot/blob/master/COPYRIGHT.txt), and are licensed under the [`CC BY 3.0`](https://creativecommons.org/licenses/by/3.0/) license. This applies to the doc comments on the serializable structs, so these are not relicensed under the licenses of this repository. The schema for the `.gdextension` file comes from the `Godot Engine` which is licensed under the [`MIT`](https://github.com/godotengine/godot/blob/master/LICENSE.txt) license.
127//! - This crate is meant to work in tandem with [`godot-rust`](https://godot-rust.github.io/) to give the most painless use of [`Rust`](https://www.rust-lang.org/) for `Godot`'s `GDExtension`, automating a helpful bunch of the work. It could be use on its own, just to generate the `.gdextension` file, but it works best with it.
128//! - The explanation on the `WindowsABI::LLVM` documentation, taken from the [`rustc` documentation](https://doc.rust-lang.org/rustc/platform-support/pc-windows-gnullvm.html), which is licensed under the [`MIT`](https://github.com/rust-lang/rust/blob/master/LICENSE-MIT) license.
129//! ## Asset licenses
130//! - The default GDExt Rust node's icons, `NodeRustSmall.svg`, `NodeRustLarge.svg` and `NodeRustFerris.svg` are licensed under the [CC BY 4.0 license](https://creativecommons.org/licenses/by/4.0/), copyright by [burritobandit28](https://github.com/burritobandit28), so they is not relicensed under the licenses of this repository. They are derived from the following works:
131//! - `Rust` `Ferris`, made by [Karen Rustad Tölva](rustacean.net) and licensed under the [`CC0 1.0 Universal`](https://creativecommons.org/publicdomain/zero/1.0/) license.
132//! - `Ferris` emoji, made by [Dzuk](https://weirder.earth/@dzuk) and licensed under the [`CC BY-NC-SA 4.0`](https://creativecommons.org/licenses/by-nc-sa/4.0/) license.
133//! - `Godot` logo, made by [Andrea Calabró](https://godotengine.org) and licensed under the [`CC BY 4.0`](https://creativecommons.org/licenses/by/4.0/) license.
134//! - `godot-rust` `Ferris`, licensed under the [`CC BY-NC-SA 4.0`](https://creativecommons.org/licenses/by-nc-sa/4.0) license, from [`godot-rust`](godot-rust.github.io).
135
136#![cfg_attr(docsrs, feature(doc_auto_cfg))]
137
138use std::{
139    env::var,
140    ffi::OsString,
141    fs::File,
142    io::{Error, ErrorKind, Result, Write},
143    path::PathBuf,
144};
145
146use args::{BaseDirectory, EntrySymbol};
147use features::sys::WindowsABI;
148use gdext::{config::Configuration, GDExtension};
149
150#[cfg(feature = "dependencies")]
151use features::target::Target;
152#[cfg(feature = "dependencies")]
153use std::collections::HashMap;
154#[cfg(feature = "dependencies")]
155use toml_edit::{table as toml_table, value as toml_value, DocumentMut};
156
157#[cfg(feature = "icons")]
158use args::icons::IconsConfig;
159
160pub mod args;
161pub mod features;
162pub mod gdext;
163pub mod prelude {
164    #[cfg(feature = "find_icons")]
165    pub use super::args::icons::{DefaultNodeIcon, NodeRust};
166    #[cfg(feature = "icons")]
167    pub use super::args::icons::{IconsConfig, IconsCopyStrategy, IconsDirectories};
168    pub use super::{
169        args::{BaseDirectory, EntrySymbol},
170        features::{
171            arch::Architecture,
172            mode::Mode,
173            sys::{System, WindowsABI},
174            target::Target,
175        },
176        gdext::config::Configuration,
177        generate_gdextension_file,
178    };
179}
180
181#[cfg(all(
182    feature = "checked_generation",
183    feature = "forced_generation",
184    not(docsrs)
185))]
186compile_error!("The features that select the kind of generation are mutually exclusive, you either use the checked or the forced one, but you can't use both. Deactivate \"checked_generation\" or \"forced_generation\".");
187
188/// SVG representations of the default GDExtension Rust nodes.
189///
190/// # Author
191/// [burritobandit28](https://github.com/burritobandit28)
192///
193/// # License
194/// [CC BY 4.0 license](https://creativecommons.org/licenses/by/4.0/)
195#[cfg(feature = "icons")]
196const NODES_RUST: [&str; 3] = [
197    include_str!("assets/NodeRustSmall.svg"),
198    include_str!("assets/NodeRustLarge.svg"),
199    include_str!("assets/NodeRustFerris.svg"),
200];
201
202/// Name of the NodeRust files.
203#[cfg(feature = "icons")]
204pub const NODES_RUST_FILENAMES: [&str; 3] = [
205    "NodeRustSmall.svg",
206    "NodeRustLarge.svg",
207    "NodeRustFerris.svg",
208];
209
210/// Generates the `.gdextension` file for the crate using all the necessary information.
211///
212/// # Parameters
213///
214/// * `base_dir` - The base directory to use for the paths in the `.gdextension` file.
215/// * `target_dir` - Path to the target directory of the crate, **relative** to the *`base_dir`*. If [`None`] is provided, defaults to `"../rust/target"`, the path provided in the `godot-rust` book.
216/// * `gdextension_path` - Path where the `.gdextension` file will be written in, **relative** to the *crate folder*. If [`None`] is provided, defaults to `"../godot/rust.gdextension"`, the path provided in the `godot-rust` book.
217/// * `force_generation` - Whether or not to generate the file even if it already exists. Available with feature "checked_generation".
218/// * `configuration` - [`Configuration`] section of the `.gdextension` file. If [`None`] is provided, defaults to the one found in the `godot-rust` book.
219/// * `windows_abi` - `ABI` used when compiling the crate for `Windows`. If [`None`] is provided, defaults to [`MSVC`](WindowsABI::MSVC), the default for `Rust` in `Windows`.
220/// * `icons_configuration` - Configuration for the generation of the icon section of the `.gdextension` file. If [`None`] is provided, it doesn't generate the icons section. Available with feature "icons".
221/// * `dependencies` - Configuration for the generation of the dependencies section of the `.gdextension` file, comprised of the targets that have dependencies and the paths (**relative** to the *`base_dir`*) of all the dependencies. If [`None`] is provided, it doesn't generate the dependencies section. Available with feature "dependencies".
222///
223/// # Returns
224/// * [`Ok`] - If the generation was successful and no IO errors or TOML errors happened.
225/// * [`Err`] - If there has been a problem writing or serializing the TOML file, copying the necessary icons or reading the source to find the associations `ClassName: IconPath` for the icons.
226pub fn generate_gdextension_file(
227    base_dir: BaseDirectory,
228    target_dir: Option<PathBuf>,
229    gdextension_path: Option<PathBuf>,
230    #[cfg(feature = "checked_generation")] force_generation: bool,
231    configuration: Option<Configuration>,
232    windows_abi: Option<WindowsABI>,
233    #[cfg(feature = "icons")] icons_configuration: Option<IconsConfig>,
234    #[cfg(feature = "dependencies")] dependencies: Option<HashMap<Target, Vec<PathBuf>>>,
235) -> Result<()> {
236    // Default values for the parameters.
237
238    // If the generation is neither forced nor checked, it's assumed to only be written when no file exists.
239    #[cfg(not(any(feature = "forced_generation", feature = "checked_generation")))]
240    let force_generation = true;
241
242    // Defaults to the provided path in the `godot-rust` book.
243    let gdextension_path = if let Some(gdextension_path) = gdextension_path {
244        if let Some(extension) = gdextension_path.extension() {
245            if extension != "gdextension" {
246                return Err(Error::new(
247                    ErrorKind::InvalidInput,
248                    "The extension of the file must be gdextension.",
249                ));
250            }
251        } else if gdextension_path
252            .file_name()
253            .unwrap_or(OsString::from("").as_os_str())
254            != ".gdextension"
255        {
256            return Err(Error::new(
257                ErrorKind::InvalidInput,
258                "The path to the gdextension file must lead to .gdextension a file.",
259            ));
260        }
261        gdextension_path
262    } else {
263        PathBuf::from_iter(["..", "godot", "rust.gdextension"])
264    };
265
266    // If the generation is not forced and the file exists.
267    #[cfg(not(feature = "forced_generation"))]
268    if !force_generation & gdextension_path.exists() {
269        return Ok(());
270    }
271
272    // Name of the library in snake_case.
273    let lib_name =
274        var("CARGO_PKG_NAME").map_or("rust".into(), |entry_symbol| entry_symbol.replace('-', "_"));
275
276    // Defaults to the provided path in the `godot-rust` book.
277    let target_dir = target_dir.unwrap_or(PathBuf::from_iter(["..", "rust", "target"]));
278
279    // Defaults to the provided configuration in the `godot-rust`.
280    let configuration = configuration.unwrap_or(Configuration::new(
281        EntrySymbol::GodotRustDefault,
282        Some((4, 1)),
283        None,
284        true,
285        false,
286    ));
287
288    // Defaults to `MSVC` since it's `Rust`'s default too.
289    let windows_abi = windows_abi.unwrap_or(WindowsABI::MSVC);
290
291    let mut gdextension = GDExtension::from_config(configuration);
292
293    gdextension.generate_libs(base_dir, lib_name.as_str(), windows_abi, target_dir);
294
295    #[cfg(feature = "icons")]
296    if let Some(mut icons_configuration) = icons_configuration {
297        if icons_configuration.directories.relative_directory.is_none() {
298            icons_configuration.directories.relative_directory = Some(base_dir)
299        }
300        gdextension.generate_icons(icons_configuration)?;
301    }
302
303    // A TOML Error gets associated with the InvalidData IO ErrorKind.
304    #[allow(unused_mut)]
305    let mut toml_string = match toml::to_string_pretty(&gdextension) {
306        Ok(toml) => toml,
307        Err(e) => return Err(Error::new(ErrorKind::InvalidData, e)),
308    };
309
310    #[cfg(feature = "dependencies")]
311    if let Some(dependencies) = dependencies {
312        let mut toml_document = toml_string
313            .parse::<DocumentMut>()
314            .expect("Invalid toml that was just parsed.");
315
316        toml_document["dependencies"] = toml_table();
317
318        for (target, dependencies) in GDExtension::generate_deps(base_dir, dependencies) {
319            toml_document["dependencies"][target] = toml_value(dependencies);
320        }
321
322        toml_document["dependencies"]
323            .as_table_like_mut()
324            .expect("The dependencies are a table, it should be tablelike.")
325            .sort_values();
326
327        // Newline after sections.
328        /*for (_, table) in toml_document.iter_mut() {
329            table.as_table_mut().unwrap().decor_mut().set_suffix("\n");
330        }*/
331
332        toml_string = toml_document.to_string();
333    }
334
335    File::create(gdextension_path)?.write(toml_string.as_bytes())?;
336
337    Ok(())
338}