The CLI
rusteal-cli builds two binaries: rusteal, which game developers install, and cargo-rusteal, which makes every command a Cargo subcommand. This chapter is how the commands work inside, file by file. What they do for a game developer is in the game developer’s handbook’s CLI reference.
Commands (src/main.rs)
The command line is a clap Parser; rusteal --version is the crate’s version.
| Command | Flags | Code |
|---|---|---|
new <Name> | --dir (.), --template (blank), --variant, --runtime-path, --feature (repeatable), --no-build, --optimizations on|off | new_cmd::run_new |
plugin new <Name> | --template (blank), --project, --no-build | plugin_cmd::run_plugin_new |
plugin package <Name> | --project, --output (<root>/Packaged/Plugins) | package_cmd::run_plugin_package |
setup <project> | setup::run_setup | |
build [project] | --all, --step N, --from N, --release, --plugin | build_cmd::run_build |
watch [project] | --plugin, --optimizations | watch_cmd::run_watch |
generate [project] | rusteal_codegen::run_generate, then run_generate_plugin for each plugin | |
package [project] | --output (<root>/Packaged), --shipping, --optimizations | package_cmd::run_package |
upgrade [project] | upgrade_cmd::run_upgrade | |
sync-plugin | sync_plugin::run_sync |
- Where a build starts (
first_build_step):--from Nstarts at step N;--alland--stepstart at 1 (--stepruns that step only); otherwise step 4 (build_cmd::RUST_STEP), so a plainrusteal buildruns the Rust steps, 4 and 5.--allconflicts with--stepand--from, and both must be 1 to 5. - The project (
project_root) is the first directory holding a.uproject, walking up from the given one or the current one (rusteal_codegen::config::find_project_root). - The version check runs before every command except
new,upgradeandsync-plugin(see Versions).
src/bin/cargo-rusteal.rs drops the rusteal argument cargo inserts, looks for the rusteal binary next to itself and then on PATH, and execs it on Unix (spawns it and forwards the exit code elsewhere). cargo rusteal watch is therefore rusteal watch, the same command line on every platform, which is what IDE run configurations use.
The build (src/build_cmd.rs)
HostPlatform::CURRENT is chosen with #[cfg(target_os)]: the CLI only builds for the platform it was compiled for.
| Platform | UBT | UAT | Library |
|---|---|---|---|
| Linux | Engine/Build/BatchFiles/Linux/Build.sh | RunUAT.sh | lib<name>.so |
| Windows | Engine/Build/BatchFiles/Build.bat (Win64) | RunUAT.bat | <name>.dll |
| Mac | Engine/Build/BatchFiles/Mac/Build.sh | RunUAT.sh | lib<name>.dylib |
The libraries (BuildContext::new):
- the game’s, unless
--pluginis given: the workspace<root>/Rust, the crate and features ofrusteal.toml’s[project], deployed to<root>/Plugins/Rusteal/Binaries/<Platform>/with the stemrusteal; - one per Rusteal plugin, every
<root>/Plugins/*holding arusteal.toml(find_plugins, sorted by name): the workspace<plugin>/Rust, the[plugin]crate and features, deployed to<plugin>/Binaries/<Platform>/with the stemrusteal_<Plugin>.--pluginmatches a name case-insensitively.
The five steps:
- UE build.
<engine>/<UBT> <Project>Editor <Platform> Development -Project=<uproject> -NoHotReload(run_ubt; on Windowscanonical_no_prefixstrips the\\?\prefix). UHT runs the exporter during it; the step then copiesrusteal_classes.json,rusteal_structs.jsonandrusteal_enums.jsonfromPlugins/RustealGenerator/Intermediate/Build/<Platform>/UnrealEditor/Inc/RustealGenerator/UHT/to<root>/Intermediate/Rusteal/uht/. - Codegen.
run_generatefor the game,run_generate_pluginfor each plugin (see Code generation). - UE rebuild. UBT again, which compiles the generated wrappers.
- Cargo. It needs
<workspace>/bindings/src/func_ids.rs(missing: “runrusteal build --allonce”), then runscargo build --manifest-path <workspace>/<crate>/Cargo.toml(or the workspace’s with-p <crate>), with--releaseand--featuresas asked. - Deploy. The library in
target/<debug|release>/is copied to<dest>.partialand renamed overlibrusteal.so(librusteal_<Plugin>.so). The deployed file is replaced, a new inode, never written into: the plugin hard-links it into its load copy (see Hot reload), and the editor’s directory watcher must never see a half-written library.
-NoHotReload keeps UBT from producing hot-reload module names, so a build with the editor open does not fight the editor’s own modules. The CLI does not check whether the editor is running: steps 1 to 3 rebuild C++ the open editor has loaded, which is why changing the plugin or the bindings needs an editor restart, and steps 4 and 5 are made to run with it open.
The profiles come from the template’s Rust/Cargo.toml (see the game developer’s handbook, Development and release builds): dev is opt-level = 1 for the game crate, 3 with line-tables-only debug info for the dependencies and the bindings (full debug info makes the library pass 100 MB, reloaded on every change); release is codegen-units = 1 and thin LTO.
run_cmd echoes the command line (cut at 200 characters) and exits with the child’s code on failure; run_cmd_code returns the code instead, for callers that must clean up first (package).
Watching (src/watch_cmd.rs)
Polling, so it needs no dependency and behaves the same on every platform: every 400 ms (POLL) it snapshots (mtime, size) of the .rs and .toml files of the game’s Rust/ and each plugin’s (--plugin filters), skipping target, bindings and directories starting with .. On a change it re-snapshots every 300 ms (SETTLE) until two agree, prints the changed files (up to three), and runs <itself> build <root> [--plugin P] as a child process. A failed build is logged and the loop goes on; it never returns. Changes to rusteal.toml or the engine need rusteal build --all, which watch does not run. It writes the platform optimizations once, when it starts.
Setting up a project (src/setup.rs)
- Writes
PLUGIN_FILES(the embedded plugins) under<project>/Plugins/, skipping files already identical. - Writes
Plugins/RustealGenerator/Source/RustealExporter/RustealExporter.ubtplugin.csproj.propswith<EngineDir>, so the exporter’s C# project finds the engine. - Without a
rusteal.toml, renders the blank template’s (templates::render_rusteal_toml), with the crate namecrate_name_forderives from the project (MyGame→my-game). - Adds
RustealandRustealGeneratorto the.uproject’sPlugins. - Unless
RustealFuncIds.hexists, writes C++ stubs inPlugins/Rusteal/Source/Rusteal/Generated/(RustealFuncIds.hwithFUNC_COUNT = 0,RustealFillFuncTable.cpp,module_deps.txtwithCore,CoreUObject,Engine), so the plugin compiles before codegen ever ran; plugins get the same with the prefix<Plugin>_.
The project’s .gitignore comes from the templates (.gitignore.tera), not from setup.
Creating a project (src/new_cmd.rs)
- The name: ASCII letters and digits, starting with a letter, at most 20 characters (Unreal’s limit for project names).
- The template and variant (
templates::select), then the features: the template’sfeaturesand the--featureones, without duplicates. - The engine template (
Template::load) is read with the engine’s own recipe,Templates/<engine_template>/Config/TemplateDefs.ini:FoldersToIgnore,FilesToIgnore,FolderRenames,FilenameReplacements,ReplacementsInFiles(case-insensitive unlessbCaseSensitive),SharedContentPacks. A template withoutSource/is Blueprint-only; the headers ofSource/<Template>/named after it are the classes to redirect. - Instantiation copies the engine template, skipping the ignored paths, its
.uprojectand the manifest’sexclude, applies the renames and, incpp,h,iniandcsfiles, the replacements (%TEMPLATENAME%,%PROJECTNAME%and their cases), and copies the shared packs intoContent/<Pack>. - The
.uproject:EngineAssociationfromEngine/Build/Build.version, modules renamed; a Blueprint-only template gets aModulesentry andwrite_game_modulewritesSource/<Project>/(two targets, aBuild.cson Core, CoreUObject, Engine, InputCore and EnhancedInput, the module files). - The project ini: a
ProjectIDwhenDefaultGame.inihas none;ActiveGameNameRedirectsand oneActiveClassRedirectsper template class inDefaultEngine.ini; the manifest’sdefault_mapasEditorStartupMapandGameDefaultMap. - The Rusteal files of the template, rendered with
project,crate_name,version,glam_versionand, with--runtime-path(which must holdrusteal-runtime/Cargo.toml),runtime_path. - The bindings placeholder crate, the platform optimizations,
setup, the features (a plugin throughplugin_cmd::create_plugin, files throughtemplates::write_project_feature_files), then the five build steps unless--no-build, and the next steps.
Templates and embedding (build.rs, src/templates.rs)
build.rs embeds everything the CLI writes into the binary, with include_bytes!:
| Generated file | Constant | From |
|---|---|---|
plugin_files.rs | PLUGIN_FILES | ../ue_plugin, or ue_plugin_embed/ in the published crate |
template_files.rs | TEMPLATE_FILES | templates/ |
plugin_template_files.rs | PLUGIN_TEMPLATE_FILES | plugin_templates/ |
project_feature_files.rs | PROJECT_FEATURE_FILES | project_features/ |
It skips Generated, Binaries, Intermediate, obj and *.csproj.props. In the repository it reads ue_plugin/ directly; the published crate has no ue_plugin/ next to it, so it reads the snapshot rusteal sync-plugin writes into rusteal-cli/ue_plugin_embed/ (gitignored, written by the release workflows). A local snapshot can be stale without harm, since the workspace build prefers ue_plugin/.
templates.rs reads the manifests: a template is <name>/<variant>/template.toml (Manifest, deny_unknown_fields: description, engine_template, exclude, default_map, features, next_step), with a mandatory base; names match loosely (case and -/_ ignored). A plugin template’s PluginManifest may carry [feature] (plugin, shared_packs); a project feature has a ProjectFeatureManifest. write_files renders {{ }} in paths and *.tera files with Tera, dropping the suffix. How to add one is in Template directories.
Plugins and packaging (src/plugin_cmd.rs, src/package_cmd.rs)
plugin new: a name of 1 to 32 letters and digits, starting with a letter, notRusteal,RustealGenerator,RustealEditornor the project’s; a feature carryingContent/keeps its fixed plugin name. It renders the template intoPlugins/<Name>/(contextplugin,crate_name,version,glam_version, andruntime_pathwhen the project follows a checkout), writes the bindings placeholder and the prefixed C++ stubs, enables the plugin in the.uproject, copies its shared packs and builds.package: the five steps in release, aSnapshotof the host’sConfig/<Platform>/*.ini, the platform optimizations,RunUAT BuildCookRun -project= -platform= -clientconfig=<Development|Shipping> -build -cook -stage -pak -archive -archivedirectory=<out> -nop4 -unattended -utf8output, the snapshot restored, then UAT’s exit code.plugin package: a release build of that plugin, then a copy of its directory withoutIntermediate,Saved,DerivedDataCache,Rust/target,obj, and the_hot_,.debugand.pdbfiles ofBinaries/.
Versions (src/project_version.rs, src/upgrade_cmd.rs)
A project’s pins are rusteal-runtime, rusteal-core and rusteal-ffi in Rust/Cargo.toml’s [workspace.dependencies], all three either exact ("=X.Y.Z", Pins::Exact) or paths into a checkout (Pins::Checkout, relative paths resolved from <root>/Rust), and in agreement. compare gives Matches, ProjectOlder, ProjectNewer or OtherCheckout: a checkout project matches only the CLI built from that same checkout (CARGO_MANIFEST_DIR/..). check looks at the project and every plugin; with Scope::PinsAndPlugins it also wants VersionName of both .uplugins to be the CLI’s. Every refusal names its fix (rusteal upgrade, cargo install rusteal@X, cargo run --manifest-path ..., rusteal setup).
upgrade refuses checkout projects and newer projects, then rewrites only the three pin lines in the game’s and every plugin’s workspace to "=<CLI version>", deletes Plugins/Rusteal and Plugins/RustealGenerator, runs setup and the full build.
The engine path (src/global_config.rs)
<config dir>/rusteal/config.toml (~/.config/rusteal/config.toml on Linux) holds [engine] path. When it is empty, the CLI asks once for the engine root, until the directory has Engine/Build/BatchFiles, and saves it; without a terminal it exits with the setting to write. This is the only per-machine setting a game project depends on; contributors have .env for theirs, which the CLI never reads.
Tests
| File | What it protects |
|---|---|
tests/plugin_version.rs | Both .uplugins carry the workspace version (VersionName) and its integer form (Version), which the runtime handshake and the version check rely on. |
main.rs | The clap definition, the version, the default build steps, --all’s conflicts, repeated --feature, the --optimizations switch. |
build_cmd.rs | The atomic deploy leaves no .partial. |
watch_cmd.rs | Ignored paths, detected changes, the change description. |
templates.rs | The template list, valid manifests, bundled features that exist, the default optimizations in every rusteal.toml.tera, the feature invariants, the RustRover files, loose names, rendered paths. |
new_cmd.rs | Writable copies, the game module of Blueprint-only templates, the default map. |
package_cmd.rs | The plugin package exclusions. |
project_version.rs, upgrade_cmd.rs | Pin parsing and comparison; rewrite_pins touches only the pins. |
optimizations.rs | The optimizations’ block, user values, untouched files, the snapshot. |
bin/cargo-rusteal.rs | Argument forwarding. |
setup.rs, global_config.rs and sync_plugin.rs have no tests: they are exercised by rusteal new in a test project (see Testing a change).