Skip to main content

rusteal_codegen/
config.rs

1// Project configuration: `<Project>/rusteal.toml`, next to the .uproject.
2//
3// The file is versioned with the project and holds only what varies between
4// projects: the game crate and the codegen module selection. Everything else
5// is a fixed layout (see `ProjectLayout`). The engine location is per machine
6// and lives in the CLI's own configuration, not here.
7
8use serde::Deserialize;
9use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12/// Name of the project configuration file.
13pub const FILE_NAME: &str = "rusteal.toml";
14
15/// Top-level `rusteal.toml`.
16#[derive(Deserialize)]
17pub struct ProjectConfig {
18    pub project: ProjectSection,
19    pub codegen: CodegenConfig,
20}
21
22#[derive(Deserialize)]
23pub struct ProjectSection {
24    /// Cargo package, a cdylib member of `Rust/`, deployed as the Rusteal library.
25    #[serde(rename = "crate")]
26    pub crate_name: String,
27    /// Extra cargo features to build it with.
28    #[serde(default)]
29    pub features: Vec<String>,
30}
31
32/// A Rusteal plugin's `rusteal.toml`, at the plugin's root
33/// (`Plugins/<Name>/rusteal.toml`): its library and the modules it binds.
34#[derive(Deserialize)]
35pub struct PluginConfig {
36    pub plugin: PluginSection,
37    pub codegen: CodegenConfig,
38}
39
40#[derive(Deserialize)]
41pub struct PluginSection {
42    /// Cargo package, a cdylib member of the plugin's `Rust/`.
43    #[serde(rename = "crate")]
44    pub crate_name: String,
45    /// Extra cargo features to build it with.
46    #[serde(default)]
47    pub features: Vec<String>,
48    /// Where the library runs: in games and the editor, or in the editor
49    /// only. It is the type of the plugin's C++ module.
50    #[serde(default)]
51    pub kind: LibraryKind,
52}
53
54/// Where a library runs.
55#[derive(Deserialize, Default, Clone, Copy, Debug, PartialEq, Eq)]
56#[serde(rename_all = "lowercase")]
57pub enum LibraryKind {
58    /// Games and the editor (a `Runtime` module).
59    #[default]
60    Runtime,
61    /// The editor only (an `Editor` module): it may bind editor modules.
62    Editor,
63}
64
65impl PluginConfig {
66    /// Read `rusteal.toml` from a plugin's root.
67    pub fn load(plugin_dir: &Path) -> Result<Self, String> {
68        let path = plugin_dir.join(FILE_NAME);
69        let text = std::fs::read_to_string(&path)
70            .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
71        toml::from_str(&text).map_err(|e| format!("cannot parse {}: {e}", path.display()))
72    }
73}
74
75#[derive(Deserialize, Clone)]
76pub struct CodegenConfig {
77    pub features: Vec<String>,
78    pub modules: HashMap<String, ModuleMapping>,
79    #[serde(default)]
80    pub blocklist: Blocklist,
81}
82
83#[derive(Deserialize, Clone)]
84pub struct ModuleMapping {
85    pub module: String,
86    pub feature: String,
87    /// The engine plugin the UE module belongs to (`StateTree`), for a module
88    /// outside the engine's own: the Rusteal plugin must list it.
89    #[serde(default)]
90    pub plugin: Option<String>,
91    /// Only these classes of the module (and the ones they derive from),
92    /// for a module much bigger than what a library needs (`UnrealEd`).
93    /// Absent: every class.
94    #[serde(default)]
95    pub classes: Option<Vec<String>>,
96}
97
98#[derive(Deserialize, Default, Clone)]
99pub struct Blocklist {
100    #[serde(default)]
101    pub classes: Vec<String>,
102    #[serde(default)]
103    pub structs: Vec<String>,
104    /// Function blocklist in "Class.Function" format.
105    #[serde(default)]
106    pub functions: Vec<String>,
107}
108
109/// Rusteal's own UE classes no library binds: they are the plugin's
110/// machinery, not parents for Rust classes.
111const RUSTEAL_INTERNAL_CLASSES: &[&str] =
112    &["RustealReifiedClass", "RustealReifiedFunction", "RustealDelegateProxy"];
113
114impl CodegenConfig {
115    /// The configuration a library of `kind` is generated with: the one in
116    /// `rusteal.toml` plus Rusteal's own modules, whose classes are parents
117    /// for Rust classes (subsystems), always bound — the editor one for
118    /// editor libraries only, as games do not have it.
119    pub fn for_library(&self, kind: LibraryKind) -> CodegenConfig {
120        let mut config = self.clone();
121        let core = config.features.first().cloned().unwrap_or_else(|| "core".to_string());
122        let mut add = |package: &str, module: &str| {
123            config.modules.entry(package.to_string()).or_insert_with(|| ModuleMapping {
124                module: module.to_string(),
125                feature: core.clone(),
126                plugin: None,
127                classes: None,
128            });
129        };
130        add("Rusteal", "rusteal");
131        if kind == LibraryKind::Editor {
132            add("RustealEditor", "rusteal_editor");
133        }
134        for class in RUSTEAL_INTERNAL_CLASSES {
135            if !config.blocklist.classes.iter().any(|c| c == class) {
136                config.blocklist.classes.push(class.to_string());
137            }
138        }
139        config
140    }
141}
142
143impl Blocklist {
144    /// Parse function blocklist entries into (class, function) tuples.
145    pub fn function_tuples(&self) -> Vec<(String, String)> {
146        self.functions
147            .iter()
148            .filter_map(|entry| {
149                let (class, func) = entry.split_once('.')?;
150                Some((class.to_string(), func.to_string()))
151            })
152            .collect()
153    }
154}
155
156impl ProjectConfig {
157    /// Read `rusteal.toml` from the project root.
158    pub fn load(root: &Path) -> Result<Self, String> {
159        let path = root.join(FILE_NAME);
160        let text = std::fs::read_to_string(&path)
161            .map_err(|e| format!("cannot read {}: {e}", path.display()))?;
162        toml::from_str(&text).map_err(|e| format!("cannot parse {}: {e}", path.display()))
163    }
164}
165
166/// Where things live inside a Rusteal project. Fixed by convention so that the
167/// config, the CLI and the plugin agree without a single path in `rusteal.toml`.
168pub struct ProjectLayout {
169    /// The directory holding the .uproject.
170    pub root: PathBuf,
171}
172
173impl ProjectLayout {
174    pub fn new(root: &Path) -> Self {
175        Self { root: root.to_path_buf() }
176    }
177
178    /// The .uproject file, if the root holds one.
179    pub fn uproject(&self) -> Option<PathBuf> {
180        find_uproject(&self.root)
181    }
182
183    /// Cargo workspace with the game crate(s) and the bindings.
184    pub fn rust_workspace(&self) -> PathBuf {
185        self.root.join("Rust")
186    }
187
188    /// The generated `bindings` crate.
189    pub fn bindings_crate(&self) -> PathBuf {
190        self.rust_workspace().join("bindings")
191    }
192
193    /// The generated C++ wrappers, compiled into the Rusteal plugin.
194    pub fn cpp_generated(&self) -> PathBuf {
195        self.root.join("Plugins/Rusteal/Source/Rusteal/Generated")
196    }
197
198    /// The Rusteal plugin's descriptor in the project.
199    pub fn plugin_descriptor(&self) -> PathBuf {
200        self.root.join("Plugins/Rusteal/Rusteal.uplugin")
201    }
202
203    /// The reflection JSON the codegen reads: build output, not versioned.
204    pub fn uht_json(&self) -> PathBuf {
205        self.root.join("Intermediate/Rusteal/uht")
206    }
207
208    /// The project's `rusteal.toml`.
209    pub fn config_file(&self) -> PathBuf {
210        self.root.join(FILE_NAME)
211    }
212}
213
214/// Where one library's generated code goes. The game's library is compiled
215/// into the Rusteal plugin; a Rusteal plugin's library into the plugin's own
216/// module, with its own bindings crate.
217pub struct LibraryOutput {
218    /// The generated `bindings` crate.
219    pub bindings_crate: PathBuf,
220    /// The generated C++ wrappers, compiled into `host_module`.
221    pub cpp_generated: PathBuf,
222    /// The descriptor of the plugin `host_module` belongs to: it must list
223    /// the engine plugins the enabled modules come from.
224    pub plugin_descriptor: PathBuf,
225    /// The UE module the C++ wrappers are compiled into.
226    pub host_module: String,
227    /// Prefix of the generated C++ symbols (see `CodegenContext::cpp_prefix`).
228    pub cpp_prefix: String,
229    /// Where the library runs: an editor library also gets the engine's
230    /// editor-only functions.
231    pub kind: LibraryKind,
232}
233
234impl ProjectLayout {
235    /// The output of the game's library.
236    pub fn game_output(&self) -> LibraryOutput {
237        LibraryOutput {
238            bindings_crate: self.bindings_crate(),
239            cpp_generated: self.cpp_generated(),
240            plugin_descriptor: self.plugin_descriptor(),
241            host_module: "Rusteal".to_string(),
242            cpp_prefix: String::new(),
243            kind: LibraryKind::Runtime,
244        }
245    }
246}
247
248/// Where things live inside a Rusteal plugin, `Plugins/<Name>/`: the plugin
249/// and its one C++ module share the name.
250pub struct PluginLayout {
251    /// The plugin's directory, holding `<Name>.uplugin`.
252    pub dir: PathBuf,
253    /// The plugin's name, which is also its C++ module's.
254    pub name: String,
255}
256
257impl PluginLayout {
258    pub fn new(dir: &Path) -> Self {
259        let name = dir
260            .file_name()
261            .map(|n| n.to_string_lossy().into_owned())
262            .unwrap_or_default();
263        Self { dir: dir.to_path_buf(), name }
264    }
265
266    /// The plugin's descriptor.
267    pub fn descriptor(&self) -> PathBuf {
268        self.dir.join(format!("{}.uplugin", self.name))
269    }
270
271    /// The plugin's `rusteal.toml`.
272    pub fn config_file(&self) -> PathBuf {
273        self.dir.join(FILE_NAME)
274    }
275
276    /// Cargo workspace with the plugin's crate and its bindings.
277    pub fn rust_workspace(&self) -> PathBuf {
278        self.dir.join("Rust")
279    }
280
281    /// The plugin's C++ module.
282    pub fn module_dir(&self) -> PathBuf {
283        self.dir.join("Source").join(&self.name)
284    }
285
286    /// The deployed library's directory: the plugin's binaries for the
287    /// platform (`Binaries/Linux`).
288    pub fn binaries(&self, platform: &str) -> PathBuf {
289        self.dir.join("Binaries").join(platform)
290    }
291
292    /// The stem of the deployed library's file name (`rusteal_<Name>`), as
293    /// `RustealRegisterPluginLibrary` looks for it.
294    pub fn library_stem(&self) -> String {
295        format!("rusteal_{}", self.name)
296    }
297
298    /// The output of the plugin's library, of the given kind.
299    pub fn output(&self, kind: LibraryKind) -> LibraryOutput {
300        LibraryOutput {
301            bindings_crate: self.rust_workspace().join("bindings"),
302            cpp_generated: self.module_dir().join("Generated"),
303            plugin_descriptor: self.descriptor(),
304            host_module: self.name.clone(),
305            cpp_prefix: format!("{}_", self.name),
306            kind,
307        }
308    }
309}
310
311/// The Rusteal plugins of a project: the directories of `Plugins/` holding a
312/// `rusteal.toml`, by name.
313pub fn find_plugins(project_root: &Path) -> Vec<PluginLayout> {
314    let Ok(entries) = std::fs::read_dir(project_root.join("Plugins")) else {
315        return Vec::new();
316    };
317    let mut plugins: Vec<PluginLayout> = entries
318        .flatten()
319        .map(|entry| entry.path())
320        .filter(|dir| dir.join(FILE_NAME).is_file())
321        .map(|dir| PluginLayout::new(&dir))
322        .collect();
323    plugins.sort_by(|a, b| a.name.cmp(&b.name));
324    plugins
325}
326
327/// The .uproject file in `dir`, if any.
328pub fn find_uproject(dir: &Path) -> Option<PathBuf> {
329    std::fs::read_dir(dir)
330        .ok()?
331        .flatten()
332        .map(|entry| entry.path())
333        .find(|path| path.extension().is_some_and(|ext| ext == "uproject"))
334}
335
336/// Walk up from `start` to the first directory holding a .uproject.
337pub fn find_project_root(start: &Path) -> Option<PathBuf> {
338    let start = start.canonicalize().ok()?;
339    start
340        .ancestors()
341        .find(|dir| find_uproject(dir).is_some())
342        .map(Path::to_path_buf)
343}
344
345#[cfg(test)]
346mod tests {
347    use super::*;
348
349    fn codegen() -> CodegenConfig {
350        toml::from_str(
351            r#"
352            features = ["core", "engine"]
353            [modules]
354            Engine = { module = "engine", feature = "engine" }
355            [blocklist]
356            classes = ["Foo"]
357            "#,
358        )
359        .unwrap()
360    }
361
362    #[test]
363    fn libraries_bind_rusteal_modules() {
364        let runtime = codegen().for_library(LibraryKind::Runtime);
365        assert_eq!(runtime.modules["Rusteal"].module, "rusteal");
366        assert_eq!(runtime.modules["Rusteal"].feature, "core");
367        assert!(!runtime.modules.contains_key("RustealEditor"));
368        assert!(runtime.blocklist.classes.iter().any(|c| c == "RustealDelegateProxy"));
369        assert!(runtime.blocklist.classes.iter().any(|c| c == "Foo"));
370
371        let editor = codegen().for_library(LibraryKind::Editor);
372        assert_eq!(editor.modules["RustealEditor"].module, "rusteal_editor");
373    }
374}