Skip to content

Commit 06dcbd9

Browse files
committed
feat: new EcsactUnrealCodgen tool
1 parent 8deceb4 commit 06dcbd9

File tree

6 files changed

+353
-0
lines changed

6 files changed

+353
-0
lines changed

Tools/EcsactUnrealCodegen/.bazelrc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
common --enable_bzlmod
2+
try-import %workspace%/user.bazelrc

Tools/EcsactUnrealCodegen/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/bazel-*
2+
/external
3+
/compile_commands.json
4+
*.bazel.lock
5+
user.bazelrc

Tools/EcsactUnrealCodegen/BUILD.bazel

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
load("@aspect_bazel_lib//lib:transitions.bzl", "platform_transition_filegroup")
2+
load("@rules_cc//cc:defs.bzl", "cc_binary")
3+
4+
cc_binary(
5+
name = "EcsactUnrealCodegen",
6+
srcs = ["EcsactUnrealCodegen.cpp"],
7+
copts = ["-std=c++20"],
8+
linkopts = select({
9+
"//conditions:default": [],
10+
"@platforms//os:windows": [
11+
"-lWs2_32",
12+
"-lMswsock",
13+
"-lBcrypt",
14+
],
15+
}),
16+
deps = [
17+
"@boost.process",
18+
"@boost.program_options",
19+
],
20+
)
21+
22+
PLATFORMS = [
23+
"@zig_sdk//platform:windows_amd64",
24+
"@zig_sdk//platform:linux_amd64",
25+
"@zig_sdk//platform:linux_arm64",
26+
"@zig_sdk//platform:darwin_amd64",
27+
"@zig_sdk//platform:darwin_arm64",
28+
]
29+
30+
[
31+
platform_transition_filegroup(
32+
name = "EcsactUnrealCodegen-" + platform.split(":")[1],
33+
srcs = [":EcsactUnrealCodegen"],
34+
target_platform = platform,
35+
)
36+
for platform in PLATFORMS
37+
]
38+
39+
filegroup(
40+
name = "AllPlatforms",
41+
srcs = ["EcsactUnrealCodegen-" + platform.split(":")[1] for platform in PLATFORMS],
42+
)
Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,278 @@
1+
#include <iostream>
2+
#include <filesystem>
3+
#include <boost/process.hpp>
4+
#include <boost/program_options.hpp>
5+
6+
namespace fs = std::filesystem;
7+
namespace bp = boost::process;
8+
namespace po = boost::program_options;
9+
10+
#define SDK_PLEASE \
11+
" - please make sure the Ecsact SDK is installed correctly\n";
12+
13+
constexpr auto HELP_MSG =
14+
"This tool uses the Ecsact SDK to run Ecsact Unreal codegen plugin. It does"
15+
"this by first searching for the Ecsact Unreal plugin in your projects "
16+
"associated Unreal Engine install directory and then searches for all Ecsact "
17+
"files in your projects Source folder.\n\n"
18+
19+
// clang-format off
20+
"Typically this tool is used in your projects .uproject file in the "
21+
"PreBuildSteps. Here's an example:\n\n"
22+
" \"PreBuildSteps\": {\n"
23+
" \"Win64\": [\n"
24+
" \"EcsactUnrealCodegen $(ProjectDir) --engine-dir $(EngineDir) || exit /b 1\"\n"
25+
" ]\n"
26+
" }\n\n"
27+
// clang-format on
28+
"See https://ecsact.dev/start/unreal for more details\n\n";
29+
30+
auto find_ecsact_exe() -> std::optional<fs::path> {
31+
auto found_exe = bp::search_path("ecsact").generic_string();
32+
if(!found_exe.empty()) {
33+
return found_exe;
34+
}
35+
36+
auto env = bp::native_environment{};
37+
auto user_profile = env.find("USERPROFILE");
38+
if(user_profile != env.end()) {
39+
auto windows_apps = fs::path{user_profile->to_string()} / "AppData" /
40+
"Local" / "Microsoft" / "WindowsApps";
41+
42+
if(fs::exists(windows_apps)) {
43+
return windows_apps / "ecsact.exe";
44+
}
45+
}
46+
47+
return std::nullopt;
48+
}
49+
50+
auto proc_stdout(auto&&... args) -> std::optional<std::string> {
51+
auto stdout_stream = bp::ipstream{};
52+
auto proc = bp::child{
53+
args...,
54+
bp::std_out > stdout_stream,
55+
bp::std_err > stderr,
56+
};
57+
58+
auto full_stdout = std::string{};
59+
auto line = std::string{};
60+
while(std::getline(stdout_stream, line)) {
61+
full_stdout += line;
62+
}
63+
64+
proc.wait();
65+
66+
if(proc.exit_code() == 0) {
67+
return full_stdout;
68+
}
69+
70+
return std::nullopt;
71+
}
72+
73+
struct plugin_dir_info {
74+
fs::path plugin_path;
75+
std::string plugin_name;
76+
};
77+
78+
auto get_plugin_dir_info( //
79+
fs::path plugin_dir
80+
) -> std::optional<plugin_dir_info> {
81+
for(auto entry : fs::directory_iterator(plugin_dir)) {
82+
if(entry.path().extension() == ".uplugin") {
83+
return plugin_dir_info{
84+
.plugin_path = entry.path(),
85+
.plugin_name = entry.path().filename().replace_extension("").string(),
86+
};
87+
}
88+
}
89+
90+
return std::nullopt;
91+
}
92+
93+
auto main(int argc, char* argv[]) -> int {
94+
auto desc = po::options_description{};
95+
auto pos_desc = po::positional_options_description{};
96+
97+
// clang-format off
98+
desc.add_options()
99+
("help", "show this help message")
100+
("engine-dir", po::value<std::string>(), "the unreal engine directory this project uses")
101+
("project-path", po::value<std::string>(), "path to unreal project file or directory");
102+
// clang-format on
103+
104+
pos_desc.add("project-path", 1);
105+
106+
auto vm = po::variables_map{};
107+
po::store(
108+
po::command_line_parser{argc, argv} //
109+
.options(desc)
110+
.positional(pos_desc)
111+
.run(),
112+
vm
113+
);
114+
po::notify(vm);
115+
116+
if(vm.count("help")) {
117+
std::cout << HELP_MSG;
118+
std::cout << "USAGE: EcsactUnrealCodegen [options] <project-path>\n\n";
119+
std::cout << "OPTIONS:\n";
120+
std::cout << desc << "\n";
121+
return 1;
122+
}
123+
124+
if(vm.count("engine-dir") == 0) {
125+
std::cerr << "ERROR: --engine-dir must be specified\n";
126+
return 1;
127+
}
128+
129+
auto engine_dir = fs::path{vm.at("engine-dir").as<std::string>()};
130+
auto engine_plugins_dir = engine_dir / "Plugins" / "Marketplace";
131+
std::cout << "INFO: engine directory is '" << engine_dir << "'\n";
132+
133+
auto ecsact_plugin_info = std::optional<plugin_dir_info>{};
134+
135+
for(auto entry : fs::directory_iterator{engine_plugins_dir}) {
136+
auto info = get_plugin_dir_info(entry.path());
137+
if(!info) {
138+
continue;
139+
}
140+
141+
if(info->plugin_name == "Ecsact") {
142+
ecsact_plugin_info = info;
143+
}
144+
}
145+
146+
if(!ecsact_plugin_info) {
147+
std::cerr //
148+
<< "ERROR: unable to find the Ecsact Unreal plugin in '"
149+
<< engine_plugins_dir.generic_string() << "'\n"
150+
<< "INFO: see https://ecsact.dev/start/unreal for installation "
151+
"instructions\n";
152+
return 1;
153+
}
154+
155+
auto ecsact_unreal_codegen_plugin =
156+
ecsact_plugin_info->plugin_path.parent_path() / "Binaries" / "Win64" /
157+
"UnrealEditor-EcsactUnrealCodegenPlugin.dll";
158+
159+
if(!fs::exists(ecsact_unreal_codegen_plugin)) {
160+
std::cerr //
161+
<< "ERROR: the Ecsact Unreal plugin does not contain the built "
162+
"EcsactUnrealCodegenPlugin - please make sure you have installed the "
163+
"Ecsact Unreal plugin correctly\n";
164+
return 1;
165+
}
166+
167+
auto project_dir = fs::path{};
168+
auto project_file = fs::path{};
169+
if(vm.count("project-path") == 0) {
170+
std::cout << "INFO: project path not specified - using current directory\n";
171+
project_dir = fs::current_path();
172+
} else {
173+
project_dir = vm.at("project-path").as<std::string>();
174+
}
175+
176+
if(fs::is_directory(project_dir)) {
177+
std::cout //
178+
<< "INFO: project path is directory - searching for uproject file\n";
179+
auto uproject_files = std::vector<fs::path>{};
180+
for(auto entry : fs::directory_iterator(project_dir)) {
181+
if(entry.path().extension() == ".uproject") {
182+
uproject_files.push_back(entry.path());
183+
}
184+
}
185+
186+
if(uproject_files.empty()) {
187+
std::cerr //
188+
<< "ERROR: could not find any .uproject files in project path '"
189+
<< project_dir.generic_string() << "'\n";
190+
return 1;
191+
}
192+
193+
if(uproject_files.size() > 1) {
194+
std::cerr //
195+
<< "ERROR: found multiple .uproject files in project path '"
196+
<< project_dir.generic_string() << "'\n";
197+
return 1;
198+
}
199+
200+
project_file = fs::absolute(uproject_files.at(0));
201+
project_dir = project_file.parent_path();
202+
} else {
203+
if(project_dir.extension() != ".uproject") {
204+
std::cerr //
205+
<< "ERROR: project path '" << project_dir.generic_string()
206+
<< "' is invalid\n"
207+
<< "ERROR: project path must be a .uproject file or directory "
208+
"containing a .uproject file\n";
209+
return 1;
210+
}
211+
212+
project_file = fs::absolute(project_dir);
213+
project_dir = project_file.parent_path();
214+
}
215+
216+
assert(!project_dir.empty());
217+
assert(!project_file.empty());
218+
219+
auto env = boost::process::native_environment{};
220+
auto ecsact_cli = find_ecsact_exe();
221+
if(!ecsact_cli) {
222+
std::cerr << "ERROR: Cannot find 'ecsact' in PATH " SDK_PLEASE;
223+
return -1;
224+
}
225+
226+
std::cout << "INFO: using " << ecsact_cli->string() << "\n";
227+
228+
auto version = proc_stdout( //
229+
bp::exe(ecsact_cli->string()),
230+
bp::args("--version")
231+
);
232+
233+
if(!version) {
234+
std::cerr << "ERROR: failed to get ecsact version" SDK_PLEASE;
235+
return 1;
236+
}
237+
238+
std::cout << "INFO: ecsact version " << *version << "\n";
239+
240+
auto source_dir = project_dir / "Source";
241+
if(!fs::exists(source_dir)) {
242+
std::cout << "WARN: project 'Source' directory does not exist\n";
243+
std::cout << "INFO: ecsact codegen was not executed\n";
244+
return 0;
245+
}
246+
247+
auto ecsact_files = std::vector<fs::path>{};
248+
for(auto entry : fs::recursive_directory_iterator(source_dir)) {
249+
if(entry.path().extension() != ".ecsact") {
250+
continue;
251+
}
252+
253+
ecsact_files.push_back(entry.path());
254+
}
255+
256+
auto ecsact_codegen_args = std::vector<std::string>{"codegen"};
257+
258+
ecsact_codegen_args.emplace_back("--plugin");
259+
ecsact_codegen_args.emplace_back("cpp_header");
260+
261+
ecsact_codegen_args.emplace_back("--plugin");
262+
ecsact_codegen_args.emplace_back( //
263+
ecsact_unreal_codegen_plugin.generic_string()
264+
);
265+
266+
for(auto ecsact_file : ecsact_files) {
267+
ecsact_codegen_args.push_back(ecsact_file.generic_string());
268+
}
269+
270+
auto codegen_proc = bp::child{
271+
bp::exe(ecsact_cli->string()),
272+
bp::args(ecsact_codegen_args),
273+
};
274+
275+
codegen_proc.wait();
276+
277+
return codegen_proc.exit_code();
278+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
bazel_dep(name = "rules_cc", version = "0.0.17")
2+
bazel_dep(name = "boost.process", version = "1.83.0.bcr.1")
3+
bazel_dep(name = "aspect_bazel_lib", version = "2.13.0")
4+
bazel_dep(name = "platforms", version = "0.0.11")
5+
bazel_dep(name = "boost.program_options", version = "1.83.0.bcr.2")
6+
bazel_dep(name = "hermetic_cc_toolchain", version = "3.1.1")
7+
8+
zig_toolchains = use_extension("@hermetic_cc_toolchain//toolchain:ext.bzl", "toolchains")
9+
use_repo(zig_toolchains, "zig_sdk")
10+
11+
register_toolchains(
12+
"@zig_sdk//toolchain:windows_amd64",
13+
"@zig_sdk//toolchain:linux_amd64_musl",
14+
"@zig_sdk//toolchain:linux_arm64_musl",
15+
"@zig_sdk//toolchain:darwin_amd64",
16+
"@zig_sdk//toolchain:darwin_arm64",
17+
dev_dependency = True,
18+
)
19+
20+
bazel_dep(name = "hedron_compile_commands", dev_dependency = True)
21+
git_override(
22+
module_name = "hedron_compile_commands",
23+
commit = "204aa593e002cbd177d30f11f54cff3559110bb9",
24+
remote = "https://github.com/hedronvision/bazel-compile-commands-extractor.git",
25+
)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

0 commit comments

Comments
 (0)