-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathSConstruct
More file actions
454 lines (386 loc) · 17.5 KB
/
SConstruct
File metadata and controls
454 lines (386 loc) · 17.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
#!/usr/bin/env python
CPP_STANDARD = "c++20"
import os, subprocess, sys
from SCons.Script import ARGUMENTS, SConscript, Alias, Default, COMMAND_LINE_TARGETS, Glob
sys.path.insert(0, os.path.join(os.getcwd(), "scripts/scons_helpers"))
from submodule_check import check_and_init_submodules
# Ensure git submodules are initialized and updated before proceeding with the build
check_and_init_submodules()
# Check that the Godot project file structure is set up correctly and get the project directory path
is_downstream_project = (
os.path.basename(os.getcwd()) == "stagehand"
and os.path.basename(os.path.dirname(os.getcwd())) == "addons"
)
# Default to no extra project directory
project_path = None
PROJECT_DIRECTORY = None
# If building as a downstream project (addons/stagehand) use the downstream project directory.
# Otherwise, only include the integration test project when explicitly requested via the `integration_tests` target.
if is_downstream_project:
project_path = "../.." # Running in a downstream project where stagehand is in addons/stagehand
from godot_project import check_and_setup_project_file_structure
PROJECT_DIRECTORY = check_and_setup_project_file_structure(project_path)
elif any(str(t) == "integration_tests" for t in COMMAND_LINE_TARGETS):
project_path = "tests/integration"
PROJECT_DIRECTORY = os.path.abspath(project_path)
# - CCFLAGS are compilation flags shared between C and C++
# - CFLAGS are for C-specific compilation flags
# - CXXFLAGS are for C++-specific compilation flags
# - CPPFLAGS are for pre-processor flags
# - CPPPATH are to tell the pre-processor where to look for header files
# - CPPDEFINES are for pre-processor defines
# - LINKFLAGS are for linking flags
# Default to the debug configuration when no explicit build options are provided.
# This ensures running `scons` matches `scripts/build_debug.sh` (debug_symbols=yes optimize=debug)
if "debug_symbols" not in ARGUMENTS: ARGUMENTS["debug_symbols"] = "yes"
if "optimize" not in ARGUMENTS: ARGUMENTS["optimize"] = "debug"
# Default to the LLVM/Clang toolchain
if "use_llvm" not in ARGUMENTS: ARGUMENTS["use_llvm"] = "yes"
# Default to generating a compilation database; primarily for Clang
if "compiledb" not in ARGUMENTS: ARGUMENTS["compiledb"] = "yes"
# Only build for x86_64 on macOS
if sys.platform == "darwin" and "arch" not in ARGUMENTS: ARGUMENTS["arch"] = "x86_64"
env = SConscript("dependencies/godot-cpp/SConstruct")
# Shared build directory for object files
BUILD_DIR = "build/obj"
# Optimize for modern CPUs, including BMI2 instructions for the heightmap
if env["arch"] == "x86_64":
if env.get("is_msvc", False):
env.Append(CCFLAGS=["/arch:AVX2"])
else:
env.Append(CCFLAGS=["-march=x86-64-v3"])
def find_source_files(base_dir):
"""Recursively find C++ source files under a base directory."""
cpp_files = []
for root, dirs, files in os.walk(base_dir):
if root == base_dir and "addons" in dirs:
dirs.remove("addons")
for f in files:
if f.endswith(".cpp") or f.endswith(".cc") or f.endswith(".cxx"):
cpp_files.append(os.path.join(root, f).replace("\\", "/"))
return cpp_files
# Source code paths
stagehand_cpp_sources = find_source_files("stagehand")
# Exclude generator translation units from the main library so their `main()` implementations are only compiled into the standalone generator program.
generator_dir_prefix = os.path.join("stagehand", "utilities", "generators").replace("\\", "/")
stagehand_cpp_sources = [s for s in stagehand_cpp_sources if not s.startswith(generator_dir_prefix + "/")]
# Also include demo translation units in the main library so demo REGISTER_IN_MODULE callbacks are linked
# into the extension and available at runtime unless when building as a downstream project (addons/stagehand).
if os.path.isdir("demos") and not is_downstream_project:
stagehand_cpp_sources.extend(find_source_files("demos/ecs"))
project_cpp_sources = []
if PROJECT_DIRECTORY:
project_cpp_sources = find_source_files(os.path.join(PROJECT_DIRECTORY, "ecs"))
# Exclude repository integration test cpp sources when requested. Use forward-slash
# normalized paths for comparison since find_source_files returns forward-slash paths.
if bool(is_downstream_project):
integration_cpp_dir = os.path.join(PROJECT_DIRECTORY, "tests", "integration", "cpp").replace("\\", "/")
if integration_cpp_dir.endswith("/"):
integration_prefix = integration_cpp_dir
else:
integration_prefix = integration_cpp_dir + "/"
project_cpp_sources = [s for s in project_cpp_sources if not (s == integration_cpp_dir or s.startswith(integration_prefix))]
# If building inside a downstream project (addons/stagehand) include the project's translation units directly into the main library so any
# `REGISTER_IN_MODULE` callbacks in the downstream project are linked into the extension and available at runtime.
if bool(is_downstream_project):
stagehand_cpp_sources.extend(project_cpp_sources)
project_cpp_sources = []
# Configure include paths; only add the additional project include root if set.
cpplist = ["dependencies/godot-cpp/include", "dependencies/godot-cpp/gen/include", "dependencies/flecs/distr", "dependencies/pfr/include", ".", f"{PROJECT_DIRECTORY}/ecs"]
if PROJECT_DIRECTORY:
cpplist.append(f"{PROJECT_DIRECTORY}")
env.Append(CPPPATH=cpplist)
flecs_c_source = "dependencies/flecs/distr/flecs.c"
# Clone the env for everything *outside* of godot-cpp so our flags/defines don't leak into godot-cpp builds.
project_env = env.Clone()
# Flecs build options
FLECS_COMMON_OPTS = [
"FLECS_CPP_NO_AUTO_REGISTRATION",
# "ecs_ftime_t=double",
]
FLECS_DEVELOPMENT_OPTS = [
"FLECS_DEBUG",
]
FLECS_PRODUCTION_OPTS = [
"FLECS_NDEBUG",
"FLECS_CUSTOM_BUILD",
"FLECS_CPP",
"FLECS_DISABLE_COUNTERS",
"FLECS_LOG",
"FLECS_META",
"FLECS_PIPELINE",
"FLECS_SCRIPT",
"FLECS_SYSTEM",
"FLECS_TIMER",
]
FLECS_OPTS = FLECS_DEVELOPMENT_OPTS if env["target"] == "template_debug" else FLECS_PRODUCTION_OPTS
FLECS_WINDOWS_OPTS = [f"/D{o}" for o in (FLECS_OPTS + FLECS_COMMON_OPTS)] + ["/TC", "/DWIN32_LEAN_AND_MEAN"]
FLECS_UNIX_OPTS = [f"-D{o}" for o in (FLECS_OPTS + FLECS_COMMON_OPTS)] + ["-std=gnu99"]
# Ensure all translation units (C and C++) see the same Flecs defines (e.g. ecs_ftime_t=double)
# Convert any "name=value" strings into (name, value) tuples so SCons emits the proper -D / /D forms and handles quoting correctly across platforms.
cppdefines_list = []
for opt in (FLECS_OPTS + FLECS_COMMON_OPTS):
if "=" in opt:
name, value = opt.split("=", 1)
cppdefines_list.append((name, value))
else:
cppdefines_list.append(opt)
project_env.Append(CPPDEFINES=cppdefines_list)
def filter_cppdefines(cppdefines, remove_names):
if cppdefines is None:
return []
if isinstance(cppdefines, dict):
return {key: value for key, value in cppdefines.items() if key not in remove_names}
filtered = []
for define in cppdefines:
if isinstance(define, tuple) and len(define) >= 1:
define_name = define[0]
else:
define_name = define
if define_name in remove_names:
continue
filtered.append(define)
return filtered
# Flecs debug/prod configuration is controlled via FLECS_DEBUG/FLECS_NDEBUG and the NDEBUG that godot-cpp may add triggers a Flecs configuration warning.
project_env["CPPDEFINES"] = filter_cppdefines(
project_env.get("CPPDEFINES", []),
{"NDEBUG"},
)
# Re-add NDEBUG only for non-debug templates so standard asserts are disabled in production builds, without conflicting with Flecs' FLECS_DEBUG in template_debug.
if env["target"] != "template_debug":
project_env.Append(CPPDEFINES=["NDEBUG"])
cxx_flags = []
if env["platform"] == "windows":
if env.get("is_msvc", False):
cxx_flags=[f"/std:{CPP_STANDARD}"]
project_env.Append(LIBS=["Ws2_32", "Dbghelp"])
else: # mingw32
cxx_flags=[f"-std={CPP_STANDARD}"]
project_env.Append(LIBS=["ws2_32", "dbghelp"])
flecs_env = project_env.Clone()
flecs_c_obj = flecs_env.SharedObject(
target=os.path.join(BUILD_DIR, "flecs"),
source=[flecs_c_source],
CFLAGS=FLECS_WINDOWS_OPTS if env.get("is_msvc", False) else FLECS_UNIX_OPTS,
)
else:
cxx_flags=[f"-std={CPP_STANDARD}"]
flecs_env = project_env.Clone()
flecs_c_obj = flecs_env.SharedObject(
target=os.path.join(BUILD_DIR, "flecs"),
source=[flecs_c_source],
CFLAGS=FLECS_UNIX_OPTS,
)
# Build stagehand sources into shared build directory
stagehand_objs = []
for src in stagehand_cpp_sources:
if is_downstream_project and os.path.isabs(src):
relative_path = os.path.relpath(src, PROJECT_DIRECTORY)
else:
relative_path = os.path.relpath(src)
obj_target = os.path.join(BUILD_DIR, os.path.splitext(relative_path)[0])
stagehand_objs.extend(project_env.SharedObject(
target=obj_target,
source=src,
CXXFLAGS=project_env["CXXFLAGS"] + cxx_flags,
))
project_cpp_objs = []
for src in project_cpp_sources:
relative_path = os.path.relpath(src, f"{PROJECT_DIRECTORY}/ecs")
obj_target = os.path.join(BUILD_DIR, os.path.splitext(relative_path)[0])
project_cpp_objs.extend(project_env.SharedObject(
target=obj_target,
source=src,
CXXFLAGS=project_env["CXXFLAGS"] + cxx_flags,
))
project_objs = stagehand_objs + project_cpp_objs + [flecs_c_obj]
# Embed the class reference documentation into the binary for editor and template_debug targets
doc_data_obj = None
if env["target"] in ["editor", "template_debug", "template_release"]:
doc_data_obj = project_env.GodotCPPDocData(
target=os.path.join(BUILD_DIR, "doc_data.gen.cpp"),
source=Glob("documentation/class_reference/*.xml")
)
project_objs.append(doc_data_obj)
if env["platform"] == "macos":
library = project_env.SharedLibrary(
"bin/libstagehand.{}.{}.framework/libstagehand.{}.{}".format(
env["platform"], env["target"], env["platform"], env["target"]
),
source=project_objs,
)
elif env["platform"] == "ios":
if env["ios_simulator"]:
library = project_env.StaticLibrary(
"bin/libstagehand.{}.{}.simulator.a".format(env["platform"], env["target"]),
source=project_objs,
)
else:
library = project_env.StaticLibrary(
"bin/libstagehand.{}.{}.a".format(env["platform"], env["target"]),
source=project_objs,
)
else:
library = project_env.SharedLibrary(
"bin/libstagehand{}{}".format(env["suffix"], env["SHLIBSUFFIX"]),
source=project_objs,
)
def run_ecs_registry_generator(target, source, env):
generator_path = os.path.abspath(str(source[0]))
output_path = os.path.abspath(str(target[0]))
output_directory = os.path.dirname(output_path)
if output_directory:
os.makedirs(output_directory, exist_ok=True)
result = subprocess.run([generator_path, output_path], cwd=os.getcwd())
return result.returncode
ecs_registry_generator = project_env.Program(
target=os.path.join(BUILD_DIR, "tools", "ecs_registry_generator"),
source=stagehand_objs + [flecs_c_obj, os.path.join("stagehand", "utilities", "generators", "ecs_registry.cpp")],
CXXFLAGS=project_env["CXXFLAGS"] + cxx_flags,
)
ecs_registry_gd = project_env.Command(
target=os.path.join("generated", "ecs_registry.gd"),
source=ecs_registry_generator,
action=run_ecs_registry_generator,
)
Alias("ecs_registry", ecs_registry_gd)
def build_unit_tests(root_env, project_root, flecs_opts, cxx_flags, tests_root=None, build_dir=None, flecs_c_obj=None, stagehand_objs=None, project_env=None):
"""Build and return the unit test program."""
from SCons.Script import ARGUMENTS, Environment, File
target = root_env["target"]
if tests_root is None:
tests_dir = os.path.join(project_root, "tests", "unit")
else:
tests_dir = tests_root
tests_build_dir = os.path.join(tests_dir, "build")
output_dir = os.path.join(tests_build_dir, "stagehand_tests")
deps_dir = os.path.join(project_root, "dependencies")
flecs_distr = os.path.join(deps_dir, "flecs", "distr")
gtest_dir = os.path.join(deps_dir, "googletest", "googletest")
godotcpp_dir = os.path.join(deps_dir, "godot-cpp")
def detect_platform_key():
plat_arg = ARGUMENTS.get("platform", "")
if plat_arg:
plat = plat_arg
else:
if sys.platform == "darwin":
plat = "macos"
elif sys.platform.startswith("win"):
plat = "windows"
elif sys.platform.startswith("linux"):
plat = "linux"
else:
plat = sys.platform
# Map template_debug/template_release to debug/release for platform key
target_map = {"template_debug": "debug", "template_release": "release", "editor": "debug"}
target_key = target_map.get(target, "debug")
arch = None
try:
import platform as _platform
machine = (_platform.machine() or "").lower()
if "arm64" in machine or "aarch64" in machine or "arm64" in ARGUMENTS.get("arch", ""):
arch = "arm64"
elif "rv64" in machine or "riscv" in machine:
arch = "rv64"
elif "64" in machine:
arch = "x86_64"
else:
arch = "x86_32"
except Exception:
arch = None
if plat in ("windows", "linux", "android") and arch is not None:
return f"{plat}.{target_key}.{arch}"
return f"{plat}.{target_key}"
def find_gdextension_value(key):
gdext_path = os.path.join(project_root, "stagehand.gdextension")
if not os.path.isfile(gdext_path):
raise FileNotFoundError(f"{gdext_path} not found")
with open(gdext_path, "r", encoding="utf-8") as fh:
for raw in fh:
line = raw.strip()
if line.startswith(key + " ="):
parts = line.split("=", 1)[1].strip()
if parts.startswith("\"") or parts.startswith("'"):
return parts.strip().strip("\"").strip("'")
return parts
return None
key = detect_platform_key()
val = find_gdextension_value(key)
if val is None:
raise RuntimeError(
f"Could not determine library path from stagehand.gdextension for key '{key}'"
)
file_name = os.path.basename(val)
if not file_name.startswith("libstagehand"):
raise RuntimeError(f"Unexpected stagehand library name: {file_name}")
stagehand_suffix = file_name[len("libstagehand"):]
for ext in (".framework", ".xcframework", ".dll", ".so", ".a", ".wasm"):
if stagehand_suffix.endswith(ext):
stagehand_suffix = stagehand_suffix[: -len(ext)]
break
test_sources = find_source_files(tests_dir)
gtest_source = os.path.join(gtest_dir, "src", "gtest-all.cc")
# Clone the project environment to inherit all compiler settings, flags, and defines
test_env = project_env.Clone()
# Ensure tests are compiled with the same C++ standard as the main project
if cxx_flags:
test_env.Append(CXXFLAGS=cxx_flags)
# Add test-specific include paths (prepend so they take priority)
test_env.Prepend(CPPPATH=[
tests_dir,
project_root,
os.path.join(project_root, "dependencies", "pfr", "include"),
os.path.join(gtest_dir, "include"),
gtest_dir,
])
# Ensure Flecs and Godot headers are available (already in project_env but verify)
if flecs_distr not in test_env["CPPPATH"] and flecs_distr not in str(test_env.get("CPPPATH", [])):
test_env.Append(CPPPATH=[flecs_distr])
# GoogleTest requires exceptions
is_msvc = root_env.get("is_msvc", False)
if is_msvc:
test_env.Append(CXXFLAGS=["/EHsc"])
else:
test_env.Append(CXXFLAGS=["-fexceptions"])
# Clear OBJPREFIX for test builds
test_env["OBJPREFIX"] = ""
# Build GoogleTest with inherited configuration
gtest_obj = test_env.SharedObject(
target=os.path.join(tests_build_dir, "gtest-all"),
source=gtest_source,
)
# Build test sources only (reuse stagehand and flecs objects from main build)
test_objs = []
for src in test_sources:
rel = os.path.relpath(src, project_root)
obj_path = os.path.join(tests_build_dir, rel.replace(".cpp", ""))
test_objs.append(test_env.SharedObject(target=obj_path, source=src))
# Combine test objects with reused stagehand/flecs objects from main build
all_objs = test_objs + [gtest_obj]
if flecs_c_obj is not None:
all_objs.insert(0, flecs_c_obj)
if stagehand_objs is not None:
all_objs = stagehand_objs + all_objs
return test_env.Program(
target=output_dir,
source=all_objs,
)
Default([library, ecs_registry_gd])
# Unit tests target
test_program = SConscript(
"tests/unit/SConstruct",
exports={
"root_env": env,
"build_unit_tests": build_unit_tests,
"project_root": os.path.normpath(os.path.abspath(".")),
"build_dir": BUILD_DIR,
"flecs_c_obj": flecs_c_obj,
"stagehand_objs": stagehand_objs,
"project_env": project_env,
"flecs_opts": FLECS_OPTS + FLECS_COMMON_OPTS,
"cxx_flags": cxx_flags,
},
)
Alias("unit_tests", test_program)
# Integration tests target
Alias("integration_tests", library)