godotcpp.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. import os
  2. import platform
  3. import sys
  4. from SCons.Action import Action
  5. from SCons.Builder import Builder
  6. from SCons.Errors import UserError
  7. from SCons.Script import ARGUMENTS
  8. from SCons.Tool import Tool
  9. from SCons.Variables import BoolVariable, EnumVariable, PathVariable
  10. from SCons.Variables.BoolVariable import _text2bool
  11. from binding_generator import _generate_bindings, _get_file_list, get_file_list
  12. from build_profile import generate_trimmed_api
  13. def add_sources(sources, dir, extension):
  14. for f in os.listdir(dir):
  15. if f.endswith("." + extension):
  16. sources.append(dir + "/" + f)
  17. def get_cmdline_bool(option, default):
  18. """We use `ARGUMENTS.get()` to check if options were manually overridden on the command line,
  19. and SCons' _text2bool helper to convert them to booleans, otherwise they're handled as strings.
  20. """
  21. cmdline_val = ARGUMENTS.get(option)
  22. if cmdline_val is not None:
  23. return _text2bool(cmdline_val)
  24. else:
  25. return default
  26. def normalize_path(val, env):
  27. return val if os.path.isabs(val) else os.path.join(env.Dir("#").abspath, val)
  28. def validate_file(key, val, env):
  29. if not os.path.isfile(normalize_path(val, env)):
  30. raise UserError("'%s' is not a file: %s" % (key, val))
  31. def validate_dir(key, val, env):
  32. if not os.path.isdir(normalize_path(val, env)):
  33. raise UserError("'%s' is not a directory: %s" % (key, val))
  34. def validate_parent_dir(key, val, env):
  35. if not os.path.isdir(normalize_path(os.path.dirname(val), env)):
  36. raise UserError("'%s' is not a directory: %s" % (key, os.path.dirname(val)))
  37. def get_platform_tools_paths(env):
  38. path = env.get("custom_tools", None)
  39. if path is None:
  40. return ["tools"]
  41. return [normalize_path(path, env), "tools"]
  42. def get_custom_platforms(env):
  43. path = env.get("custom_tools", None)
  44. if path is None:
  45. return []
  46. platforms = []
  47. for x in os.listdir(normalize_path(path, env)):
  48. if not x.endswith(".py"):
  49. continue
  50. platforms.append(x.removesuffix(".py"))
  51. return platforms
  52. def no_verbose(env):
  53. colors = {}
  54. # Colors are disabled in non-TTY environments such as pipes. This means
  55. # that if output is redirected to a file, it will not contain color codes
  56. if sys.stdout.isatty():
  57. colors["blue"] = "\033[0;94m"
  58. colors["bold_blue"] = "\033[1;94m"
  59. colors["reset"] = "\033[0m"
  60. else:
  61. colors["blue"] = ""
  62. colors["bold_blue"] = ""
  63. colors["reset"] = ""
  64. # There is a space before "..." to ensure that source file names can be
  65. # Ctrl + clicked in the VS Code terminal.
  66. compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(
  67. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  68. )
  69. java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(
  70. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  71. )
  72. compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(
  73. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  74. )
  75. link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(
  76. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  77. )
  78. link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(
  79. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  80. )
  81. ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(
  82. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  83. )
  84. link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(
  85. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  86. )
  87. java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(
  88. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  89. )
  90. compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(
  91. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  92. )
  93. generated_file_message = "{}Generating {}$TARGET{} ...{}".format(
  94. colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
  95. )
  96. env.Append(CXXCOMSTR=[compile_source_message])
  97. env.Append(CCCOMSTR=[compile_source_message])
  98. env.Append(SHCCCOMSTR=[compile_shared_source_message])
  99. env.Append(SHCXXCOMSTR=[compile_shared_source_message])
  100. env.Append(ARCOMSTR=[link_library_message])
  101. env.Append(RANLIBCOMSTR=[ranlib_library_message])
  102. env.Append(SHLINKCOMSTR=[link_shared_library_message])
  103. env.Append(LINKCOMSTR=[link_program_message])
  104. env.Append(JARCOMSTR=[java_library_message])
  105. env.Append(JAVACCOMSTR=[java_compile_source_message])
  106. env.Append(RCCOMSTR=[compiled_resource_message])
  107. env.Append(GENCOMSTR=[generated_file_message])
  108. def scons_emit_files(target, source, env):
  109. profile_filepath = env.get("build_profile", "")
  110. if profile_filepath:
  111. profile_filepath = normalize_path(profile_filepath, env)
  112. # Always clean all files
  113. env.Clean(target, [env.File(f) for f in get_file_list(str(source[0]), target[0].abspath, True, True)])
  114. api = generate_trimmed_api(str(source[0]), profile_filepath)
  115. files = [env.File(f) for f in _get_file_list(api, target[0].abspath, True, True)]
  116. env["godot_cpp_gen_dir"] = target[0].abspath
  117. return files, source
  118. def scons_generate_bindings(target, source, env):
  119. profile_filepath = env.get("build_profile", "")
  120. if profile_filepath:
  121. profile_filepath = normalize_path(profile_filepath, env)
  122. api = generate_trimmed_api(str(source[0]), profile_filepath)
  123. _generate_bindings(
  124. api,
  125. env["generate_template_get_node"],
  126. "32" if "32" in env["arch"] else "64",
  127. env["precision"],
  128. env["godot_cpp_gen_dir"],
  129. )
  130. return None
  131. platforms = ["linux", "macos", "windows", "android", "ios", "web"]
  132. # CPU architecture options.
  133. architecture_array = [
  134. "",
  135. "universal",
  136. "x86_32",
  137. "x86_64",
  138. "arm32",
  139. "arm64",
  140. "rv64",
  141. "ppc32",
  142. "ppc64",
  143. "wasm32",
  144. ]
  145. architecture_aliases = {
  146. "x64": "x86_64",
  147. "amd64": "x86_64",
  148. "armv7": "arm32",
  149. "armv8": "arm64",
  150. "arm64v8": "arm64",
  151. "aarch64": "arm64",
  152. "rv": "rv64",
  153. "riscv": "rv64",
  154. "riscv64": "rv64",
  155. "ppcle": "ppc32",
  156. "ppc": "ppc32",
  157. "ppc64le": "ppc64",
  158. }
  159. def exists(env):
  160. return True
  161. def options(opts, env):
  162. # Try to detect the host platform automatically.
  163. # This is used if no `platform` argument is passed
  164. if sys.platform.startswith("linux"):
  165. default_platform = "linux"
  166. elif sys.platform == "darwin":
  167. default_platform = "macos"
  168. elif sys.platform == "win32" or sys.platform == "msys":
  169. default_platform = "windows"
  170. elif ARGUMENTS.get("platform", ""):
  171. default_platform = ARGUMENTS.get("platform")
  172. else:
  173. raise ValueError("Could not detect platform automatically, please specify with platform=<platform>")
  174. opts.Add(
  175. PathVariable(
  176. key="custom_tools",
  177. help="Path to directory containing custom tools",
  178. default=env.get("custom_tools", None),
  179. validator=validate_dir,
  180. )
  181. )
  182. opts.Update(env)
  183. custom_platforms = get_custom_platforms(env)
  184. opts.Add(
  185. EnumVariable(
  186. key="platform",
  187. help="Target platform",
  188. default=env.get("platform", default_platform),
  189. allowed_values=platforms + custom_platforms,
  190. ignorecase=2,
  191. )
  192. )
  193. # Editor and template_debug are compatible (i.e. you can use the same binary for Godot editor builds and Godot debug templates).
  194. # Godot release templates are only compatible with "template_release" builds.
  195. # For this reason, we default to template_debug builds, unlike Godot which defaults to editor builds.
  196. opts.Add(
  197. EnumVariable(
  198. key="target",
  199. help="Compilation target",
  200. default=env.get("target", "template_debug"),
  201. allowed_values=("editor", "template_release", "template_debug"),
  202. )
  203. )
  204. opts.Add(
  205. PathVariable(
  206. key="gdextension_dir",
  207. help="Path to a custom directory containing GDExtension interface header and API JSON file",
  208. default=env.get("gdextension_dir", None),
  209. validator=validate_dir,
  210. )
  211. )
  212. opts.Add(
  213. PathVariable(
  214. key="custom_api_file",
  215. help="Path to a custom GDExtension API JSON file (takes precedence over `gdextension_dir`)",
  216. default=env.get("custom_api_file", None),
  217. validator=validate_file,
  218. )
  219. )
  220. opts.Add(
  221. BoolVariable(
  222. key="generate_bindings",
  223. help="Force GDExtension API bindings generation. Auto-detected by default.",
  224. default=env.get("generate_bindings", False),
  225. )
  226. )
  227. opts.Add(
  228. BoolVariable(
  229. key="generate_template_get_node",
  230. help="Generate a template version of the Node class's get_node.",
  231. default=env.get("generate_template_get_node", True),
  232. )
  233. )
  234. opts.Add(
  235. BoolVariable(
  236. key="build_library",
  237. help="Build the godot-cpp library.",
  238. default=env.get("build_library", True),
  239. )
  240. )
  241. opts.Add(
  242. EnumVariable(
  243. key="precision",
  244. help="Set the floating-point precision level",
  245. default=env.get("precision", "single"),
  246. allowed_values=("single", "double"),
  247. )
  248. )
  249. opts.Add(
  250. EnumVariable(
  251. key="arch",
  252. help="CPU architecture",
  253. default=env.get("arch", ""),
  254. allowed_values=architecture_array,
  255. map=architecture_aliases,
  256. )
  257. )
  258. opts.Add(BoolVariable(key="threads", help="Enable threading support", default=env.get("threads", True)))
  259. # compiledb
  260. opts.Add(
  261. BoolVariable(
  262. key="compiledb",
  263. help="Generate compilation DB (`compile_commands.json`) for external tools",
  264. default=env.get("compiledb", False),
  265. )
  266. )
  267. opts.Add(
  268. PathVariable(
  269. key="compiledb_file",
  270. help="Path to a custom `compile_commands.json` file",
  271. default=env.get("compiledb_file", "compile_commands.json"),
  272. validator=validate_parent_dir,
  273. )
  274. )
  275. opts.Add(
  276. PathVariable(
  277. "build_profile",
  278. "Path to a file containing a feature build profile",
  279. default=env.get("build_profile", None),
  280. validator=validate_file,
  281. )
  282. )
  283. opts.Add(
  284. BoolVariable(
  285. key="use_hot_reload",
  286. help="Enable the extra accounting required to support hot reload.",
  287. default=env.get("use_hot_reload", None),
  288. )
  289. )
  290. opts.Add(
  291. BoolVariable(
  292. "disable_exceptions", "Force disabling exception handling code", default=env.get("disable_exceptions", True)
  293. )
  294. )
  295. opts.Add(
  296. EnumVariable(
  297. key="symbols_visibility",
  298. help="Symbols visibility on GNU platforms. Use 'auto' to apply the default value.",
  299. default=env.get("symbols_visibility", "hidden"),
  300. allowed_values=["auto", "visible", "hidden"],
  301. )
  302. )
  303. opts.Add(
  304. EnumVariable(
  305. "optimize",
  306. "The desired optimization flags",
  307. "speed_trace",
  308. ("none", "custom", "debug", "speed", "speed_trace", "size"),
  309. )
  310. )
  311. opts.Add(
  312. EnumVariable(
  313. "lto",
  314. "Link-time optimization",
  315. "none",
  316. ("none", "auto", "thin", "full"),
  317. )
  318. )
  319. opts.Add(BoolVariable("debug_symbols", "Build with debugging symbols", True))
  320. opts.Add(BoolVariable("dev_build", "Developer build with dev-only debugging code (DEV_ENABLED)", False))
  321. opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", False))
  322. # Add platform options (custom tools can override platforms)
  323. for pl in sorted(set(platforms + custom_platforms)):
  324. tool = Tool(pl, toolpath=get_platform_tools_paths(env))
  325. if hasattr(tool, "options"):
  326. tool.options(opts)
  327. def make_doc_source(target, source, env):
  328. import zlib
  329. dst = str(target[0])
  330. g = open(dst, "w", encoding="utf-8")
  331. buf = ""
  332. docbegin = ""
  333. docend = ""
  334. for src in source:
  335. src_path = str(src)
  336. if not src_path.endswith(".xml"):
  337. continue
  338. with open(src_path, "r", encoding="utf-8") as f:
  339. content = f.read()
  340. buf += content
  341. buf = (docbegin + buf + docend).encode("utf-8")
  342. decomp_size = len(buf)
  343. # Use maximum zlib compression level to further reduce file size
  344. # (at the cost of initial build times).
  345. buf = zlib.compress(buf, zlib.Z_BEST_COMPRESSION)
  346. g.write("/* THIS FILE IS GENERATED DO NOT EDIT */\n")
  347. g.write("\n")
  348. g.write("#include <godot_cpp/godot.hpp>\n")
  349. g.write("\n")
  350. g.write('static const char *_doc_data_hash = "' + str(hash(buf)) + '";\n')
  351. g.write("static const int _doc_data_uncompressed_size = " + str(decomp_size) + ";\n")
  352. g.write("static const int _doc_data_compressed_size = " + str(len(buf)) + ";\n")
  353. g.write("static const unsigned char _doc_data_compressed[] = {\n")
  354. for i in range(len(buf)):
  355. g.write("\t" + str(buf[i]) + ",\n")
  356. g.write("};\n")
  357. g.write("\n")
  358. g.write(
  359. "static godot::internal::DocDataRegistration _doc_data_registration(_doc_data_hash, _doc_data_uncompressed_size, _doc_data_compressed_size, _doc_data_compressed);\n"
  360. )
  361. g.write("\n")
  362. g.close()
  363. def generate(env):
  364. # Default num_jobs to local cpu count if not user specified.
  365. # SCons has a peculiarity where user-specified options won't be overridden
  366. # by SetOption, so we can rely on this to know if we should use our default.
  367. initial_num_jobs = env.GetOption("num_jobs")
  368. altered_num_jobs = initial_num_jobs + 1
  369. env.SetOption("num_jobs", altered_num_jobs)
  370. if env.GetOption("num_jobs") == altered_num_jobs:
  371. cpu_count = os.cpu_count()
  372. if cpu_count is None:
  373. print("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.")
  374. else:
  375. safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1
  376. print(
  377. "Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the -j argument."
  378. % (cpu_count, safer_cpu_count)
  379. )
  380. env.SetOption("num_jobs", safer_cpu_count)
  381. # Process CPU architecture argument.
  382. if env["arch"] == "":
  383. # No architecture specified. Default to arm64 if building for Android,
  384. # universal if building for macOS or iOS, wasm32 if building for web,
  385. # otherwise default to the host architecture.
  386. if env["platform"] in ["macos", "ios"]:
  387. env["arch"] = "universal"
  388. elif env["platform"] == "android":
  389. env["arch"] = "arm64"
  390. elif env["platform"] == "web":
  391. env["arch"] = "wasm32"
  392. else:
  393. host_machine = platform.machine().lower()
  394. if host_machine in architecture_array:
  395. env["arch"] = host_machine
  396. elif host_machine in architecture_aliases.keys():
  397. env["arch"] = architecture_aliases[host_machine]
  398. elif "86" in host_machine:
  399. # Catches x86, i386, i486, i586, i686, etc.
  400. env["arch"] = "x86_32"
  401. else:
  402. print("Unsupported CPU architecture: " + host_machine)
  403. env.Exit(1)
  404. print("Building for architecture " + env["arch"] + " on platform " + env["platform"])
  405. # These defaults may be needed by platform tools
  406. env.use_hot_reload = env.get("use_hot_reload", env["target"] != "template_release")
  407. env.editor_build = env["target"] == "editor"
  408. env.dev_build = env["dev_build"]
  409. env.debug_features = env["target"] in ["editor", "template_debug"]
  410. if env.dev_build:
  411. opt_level = "none"
  412. elif env.debug_features:
  413. opt_level = "speed_trace"
  414. else: # Release
  415. opt_level = "speed"
  416. env["optimize"] = ARGUMENTS.get("optimize", opt_level)
  417. env["debug_symbols"] = get_cmdline_bool("debug_symbols", env.dev_build)
  418. tool = Tool(env["platform"], toolpath=get_platform_tools_paths(env))
  419. if tool is None or not tool.exists(env):
  420. raise ValueError("Required toolchain not found for platform " + env["platform"])
  421. tool.generate(env)
  422. if env["threads"]:
  423. env.Append(CPPDEFINES=["THREADS_ENABLED"])
  424. if env.use_hot_reload:
  425. env.Append(CPPDEFINES=["HOT_RELOAD_ENABLED"])
  426. if env.editor_build:
  427. env.Append(CPPDEFINES=["TOOLS_ENABLED"])
  428. # Configuration of build targets:
  429. # - Editor or template
  430. # - Debug features (DEBUG_ENABLED code)
  431. # - Dev only code (DEV_ENABLED code)
  432. # - Optimization level
  433. # - Debug symbols for crash traces / debuggers
  434. # Keep this configuration in sync with SConstruct in upstream Godot.
  435. if env.debug_features:
  436. # DEBUG_ENABLED enables debugging *features* and debug-only code, which is intended
  437. # to give *users* extra debugging information for their game development.
  438. env.Append(CPPDEFINES=["DEBUG_ENABLED"])
  439. # In upstream Godot this is added in typedefs.h when DEBUG_ENABLED is set.
  440. env.Append(CPPDEFINES=["DEBUG_METHODS_ENABLED"])
  441. if env.dev_build:
  442. # DEV_ENABLED enables *engine developer* code which should only be compiled for those
  443. # working on the engine itself.
  444. env.Append(CPPDEFINES=["DEV_ENABLED"])
  445. else:
  446. # Disable assert() for production targets (only used in thirdparty code).
  447. env.Append(CPPDEFINES=["NDEBUG"])
  448. if env["precision"] == "double":
  449. env.Append(CPPDEFINES=["REAL_T_IS_DOUBLE"])
  450. # Allow detecting when building as a GDExtension.
  451. env.Append(CPPDEFINES=["GDEXTENSION"])
  452. # Suffix
  453. suffix = ".{}.{}".format(env["platform"], env["target"])
  454. if env.dev_build:
  455. suffix += ".dev"
  456. if env["precision"] == "double":
  457. suffix += ".double"
  458. suffix += "." + env["arch"]
  459. if env["ios_simulator"]:
  460. suffix += ".simulator"
  461. if not env["threads"]:
  462. suffix += ".nothreads"
  463. env["suffix"] = suffix # Exposed when included from another project
  464. env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"]
  465. # compile_commands.json
  466. env.Tool("compilation_db")
  467. env.Alias("compiledb", env.CompilationDatabase(normalize_path(env["compiledb_file"], env)))
  468. # Formatting
  469. if not env["verbose"]:
  470. no_verbose(env)
  471. # Builders
  472. env.Append(
  473. BUILDERS={
  474. "GodotCPPBindings": Builder(action=Action(scons_generate_bindings, "$GENCOMSTR"), emitter=scons_emit_files),
  475. "GodotCPPDocData": Builder(action=make_doc_source),
  476. }
  477. )
  478. env.AddMethod(_godot_cpp, "GodotCPP")
  479. def _godot_cpp(env):
  480. extension_dir = normalize_path(env.get("gdextension_dir", env.Dir("gdextension").abspath), env)
  481. api_file = normalize_path(env.get("custom_api_file", env.File(extension_dir + "/extension_api.json").abspath), env)
  482. bindings = env.GodotCPPBindings(
  483. env.Dir("."),
  484. [
  485. api_file,
  486. os.path.join(extension_dir, "gdextension_interface.h"),
  487. "binding_generator.py",
  488. ],
  489. )
  490. # Forces bindings regeneration.
  491. if env["generate_bindings"]:
  492. env.AlwaysBuild(bindings)
  493. env.NoCache(bindings)
  494. # Sources to compile
  495. sources = []
  496. add_sources(sources, "src", "cpp")
  497. add_sources(sources, "src/classes", "cpp")
  498. add_sources(sources, "src/core", "cpp")
  499. add_sources(sources, "src/variant", "cpp")
  500. sources.extend([f for f in bindings if str(f).endswith(".cpp")])
  501. # Includes
  502. env.AppendUnique(CPPPATH=[env.Dir(d) for d in [extension_dir, "include", "gen/include"]])
  503. library = None
  504. library_name = "libgodot-cpp" + env["suffix"] + env["LIBSUFFIX"]
  505. if env["build_library"]:
  506. library = env.StaticLibrary(target=env.File("bin/%s" % library_name), source=sources)
  507. env.NoCache(library)
  508. default_args = [library]
  509. # Add compiledb if the option is set
  510. if env.get("compiledb", False):
  511. default_args += ["compiledb"]
  512. env.Default(*default_args)
  513. env.AppendUnique(LIBS=[env.File("bin/%s" % library_name)])
  514. return library