godotcpp.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. import os, sys, platform
  2. from SCons.Variables import EnumVariable, PathVariable, BoolVariable
  3. from SCons.Tool import Tool
  4. from SCons.Builder import Builder
  5. from SCons.Errors import UserError
  6. from binding_generator import scons_generate_bindings, scons_emit_files
  7. def add_sources(sources, dir, extension):
  8. for f in os.listdir(dir):
  9. if f.endswith("." + extension):
  10. sources.append(dir + "/" + f)
  11. def normalize_path(val, env):
  12. return val if os.path.isabs(val) else os.path.join(env.Dir("#").abspath, val)
  13. def validate_file(key, val, env):
  14. if not os.path.isfile(normalize_path(val, env)):
  15. raise UserError("'%s' is not a file: %s" % (key, val))
  16. def validate_dir(key, val, env):
  17. if not os.path.isdir(normalize_path(val, env)):
  18. raise UserError("'%s' is not a directory: %s" % (key, val))
  19. def validate_parent_dir(key, val, env):
  20. if not os.path.isdir(normalize_path(os.path.dirname(val), env)):
  21. raise UserError("'%s' is not a directory: %s" % (key, os.path.dirname(val)))
  22. def get_platform_tools_paths(env):
  23. path = env.get("custom_tools", None)
  24. if path is None:
  25. return ["tools"]
  26. return [normalize_path(path, env), "tools"]
  27. def get_custom_platforms(env):
  28. path = env.get("custom_tools", None)
  29. if path is None:
  30. return []
  31. platforms = []
  32. for x in os.listdir(normalize_path(path, env)):
  33. if not x.endswith(".py"):
  34. continue
  35. platforms.append(x.removesuffix(".py"))
  36. return platforms
  37. platforms = ["linux", "macos", "windows", "android", "ios", "web"]
  38. # CPU architecture options.
  39. architecture_array = [
  40. "",
  41. "universal",
  42. "x86_32",
  43. "x86_64",
  44. "arm32",
  45. "arm64",
  46. "rv64",
  47. "ppc32",
  48. "ppc64",
  49. "wasm32",
  50. ]
  51. architecture_aliases = {
  52. "x64": "x86_64",
  53. "amd64": "x86_64",
  54. "armv7": "arm32",
  55. "armv8": "arm64",
  56. "arm64v8": "arm64",
  57. "aarch64": "arm64",
  58. "rv": "rv64",
  59. "riscv": "rv64",
  60. "riscv64": "rv64",
  61. "ppcle": "ppc32",
  62. "ppc": "ppc32",
  63. "ppc64le": "ppc64",
  64. }
  65. def exists(env):
  66. return True
  67. def options(opts, env):
  68. # Try to detect the host platform automatically.
  69. # This is used if no `platform` argument is passed
  70. if sys.platform.startswith("linux"):
  71. default_platform = "linux"
  72. elif sys.platform == "darwin":
  73. default_platform = "macos"
  74. elif sys.platform == "win32" or sys.platform == "msys":
  75. default_platform = "windows"
  76. elif ARGUMENTS.get("platform", ""):
  77. default_platform = ARGUMENTS.get("platform")
  78. else:
  79. raise ValueError("Could not detect platform automatically, please specify with platform=<platform>")
  80. opts.Add(
  81. PathVariable(
  82. key="custom_tools",
  83. help="Path to directory containing custom tools",
  84. default=env.get("custom_tools", None),
  85. validator=validate_dir,
  86. )
  87. )
  88. opts.Update(env)
  89. custom_platforms = get_custom_platforms(env)
  90. opts.Add(
  91. EnumVariable(
  92. key="platform",
  93. help="Target platform",
  94. default=env.get("platform", default_platform),
  95. allowed_values=platforms + custom_platforms,
  96. ignorecase=2,
  97. )
  98. )
  99. # Editor and template_debug are compatible (i.e. you can use the same binary for Godot editor builds and Godot debug templates).
  100. # Godot release templates are only compatible with "template_release" builds.
  101. # For this reason, we default to template_debug builds, unlike Godot which defaults to editor builds.
  102. opts.Add(
  103. EnumVariable(
  104. key="target",
  105. help="Compilation target",
  106. default=env.get("target", "template_debug"),
  107. allowed_values=("editor", "template_release", "template_debug"),
  108. )
  109. )
  110. opts.Add(
  111. PathVariable(
  112. key="gdextension_dir",
  113. help="Path to a custom directory containing GDExtension interface header and API JSON file",
  114. default=env.get("gdextension_dir", None),
  115. validator=validate_dir,
  116. )
  117. )
  118. opts.Add(
  119. PathVariable(
  120. key="custom_api_file",
  121. help="Path to a custom GDExtension API JSON file (takes precedence over `gdextension_dir`)",
  122. default=env.get("custom_api_file", None),
  123. validator=validate_file,
  124. )
  125. )
  126. opts.Add(
  127. BoolVariable(
  128. key="generate_bindings",
  129. help="Force GDExtension API bindings generation. Auto-detected by default.",
  130. default=env.get("generate_bindings", False),
  131. )
  132. )
  133. opts.Add(
  134. BoolVariable(
  135. key="generate_template_get_node",
  136. help="Generate a template version of the Node class's get_node.",
  137. default=env.get("generate_template_get_node", True),
  138. )
  139. )
  140. opts.Add(
  141. BoolVariable(
  142. key="build_library",
  143. help="Build the godot-cpp library.",
  144. default=env.get("build_library", True),
  145. )
  146. )
  147. opts.Add(
  148. EnumVariable(
  149. key="precision",
  150. help="Set the floating-point precision level",
  151. default=env.get("precision", "single"),
  152. allowed_values=("single", "double"),
  153. )
  154. )
  155. opts.Add(
  156. EnumVariable(
  157. key="arch",
  158. help="CPU architecture",
  159. default=env.get("arch", ""),
  160. allowed_values=architecture_array,
  161. map=architecture_aliases,
  162. )
  163. )
  164. # compiledb
  165. opts.Add(
  166. BoolVariable(
  167. key="compiledb",
  168. help="Generate compilation DB (`compile_commands.json`) for external tools",
  169. default=env.get("compiledb", False),
  170. )
  171. )
  172. opts.Add(
  173. PathVariable(
  174. key="compiledb_file",
  175. help="Path to a custom `compile_commands.json` file",
  176. default=env.get("compiledb_file", "compile_commands.json"),
  177. validator=validate_parent_dir,
  178. )
  179. )
  180. opts.Add(
  181. BoolVariable(
  182. key="use_hot_reload",
  183. help="Enable the extra accounting required to support hot reload.",
  184. default=env.get("use_hot_reload", None),
  185. )
  186. )
  187. opts.Add(
  188. BoolVariable(
  189. "disable_exceptions", "Force disabling exception handling code", default=env.get("disable_exceptions", True)
  190. )
  191. )
  192. opts.Add(
  193. EnumVariable(
  194. key="symbols_visibility",
  195. help="Symbols visibility on GNU platforms. Use 'auto' to apply the default value.",
  196. default=env.get("symbols_visibility", "hidden"),
  197. allowed_values=["auto", "visible", "hidden"],
  198. )
  199. )
  200. # Add platform options (custom tools can override platforms)
  201. for pl in sorted(set(platforms + custom_platforms)):
  202. tool = Tool(pl, toolpath=get_platform_tools_paths(env))
  203. if hasattr(tool, "options"):
  204. tool.options(opts)
  205. # Targets flags tool (optimizations, debug symbols)
  206. target_tool = Tool("targets", toolpath=["tools"])
  207. target_tool.options(opts)
  208. def generate(env):
  209. # Default num_jobs to local cpu count if not user specified.
  210. # SCons has a peculiarity where user-specified options won't be overridden
  211. # by SetOption, so we can rely on this to know if we should use our default.
  212. initial_num_jobs = env.GetOption("num_jobs")
  213. altered_num_jobs = initial_num_jobs + 1
  214. env.SetOption("num_jobs", altered_num_jobs)
  215. if env.GetOption("num_jobs") == altered_num_jobs:
  216. cpu_count = os.cpu_count()
  217. if cpu_count is None:
  218. print("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.")
  219. else:
  220. safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1
  221. print(
  222. "Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the -j argument."
  223. % (cpu_count, safer_cpu_count)
  224. )
  225. env.SetOption("num_jobs", safer_cpu_count)
  226. # Process CPU architecture argument.
  227. if env["arch"] == "":
  228. # No architecture specified. Default to arm64 if building for Android,
  229. # universal if building for macOS or iOS, wasm32 if building for web,
  230. # otherwise default to the host architecture.
  231. if env["platform"] in ["macos", "ios"]:
  232. env["arch"] = "universal"
  233. elif env["platform"] == "android":
  234. env["arch"] = "arm64"
  235. elif env["platform"] == "web":
  236. env["arch"] = "wasm32"
  237. else:
  238. host_machine = platform.machine().lower()
  239. if host_machine in architecture_array:
  240. env["arch"] = host_machine
  241. elif host_machine in architecture_aliases.keys():
  242. env["arch"] = architecture_aliases[host_machine]
  243. elif "86" in host_machine:
  244. # Catches x86, i386, i486, i586, i686, etc.
  245. env["arch"] = "x86_32"
  246. else:
  247. print("Unsupported CPU architecture: " + host_machine)
  248. env.Exit(1)
  249. print("Building for architecture " + env["arch"] + " on platform " + env["platform"])
  250. if env.get("use_hot_reload") is None:
  251. env["use_hot_reload"] = env["target"] != "template_release"
  252. if env["use_hot_reload"]:
  253. env.Append(CPPDEFINES=["HOT_RELOAD_ENABLED"])
  254. tool = Tool(env["platform"], toolpath=get_platform_tools_paths(env))
  255. if tool is None or not tool.exists(env):
  256. raise ValueError("Required toolchain not found for platform " + env["platform"])
  257. tool.generate(env)
  258. target_tool = Tool("targets", toolpath=["tools"])
  259. target_tool.generate(env)
  260. # Disable exception handling. Godot doesn't use exceptions anywhere, and this
  261. # saves around 20% of binary size and very significant build time.
  262. if env["disable_exceptions"]:
  263. if env.get("is_msvc", False):
  264. env.Append(CPPDEFINES=[("_HAS_EXCEPTIONS", 0)])
  265. else:
  266. env.Append(CXXFLAGS=["-fno-exceptions"])
  267. elif env.get("is_msvc", False):
  268. env.Append(CXXFLAGS=["/EHsc"])
  269. if not env.get("is_msvc", False):
  270. if env["symbols_visibility"] == "visible":
  271. env.Append(CCFLAGS=["-fvisibility=default"])
  272. env.Append(LINKFLAGS=["-fvisibility=default"])
  273. elif env["symbols_visibility"] == "hidden":
  274. env.Append(CCFLAGS=["-fvisibility=hidden"])
  275. env.Append(LINKFLAGS=["-fvisibility=hidden"])
  276. # Require C++17
  277. if env.get("is_msvc", False):
  278. env.Append(CXXFLAGS=["/std:c++17"])
  279. else:
  280. env.Append(CXXFLAGS=["-std=c++17"])
  281. if env["precision"] == "double":
  282. env.Append(CPPDEFINES=["REAL_T_IS_DOUBLE"])
  283. # Allow detecting when building as a GDExtension.
  284. env.Append(CPPDEFINES=["GDEXTENSION"])
  285. # Suffix
  286. suffix = ".{}.{}".format(env["platform"], env["target"])
  287. if env.dev_build:
  288. suffix += ".dev"
  289. if env["precision"] == "double":
  290. suffix += ".double"
  291. suffix += "." + env["arch"]
  292. if env["ios_simulator"]:
  293. suffix += ".simulator"
  294. env["suffix"] = suffix # Exposed when included from another project
  295. env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"]
  296. # compile_commands.json
  297. env.Tool("compilation_db")
  298. env.Alias("compiledb", env.CompilationDatabase(normalize_path(env["compiledb_file"], env)))
  299. # Builders
  300. env.Append(BUILDERS={"GodotCPPBindings": Builder(action=scons_generate_bindings, emitter=scons_emit_files)})
  301. env.AddMethod(_godot_cpp, "GodotCPP")
  302. def _godot_cpp(env):
  303. extension_dir = normalize_path(env.get("gdextension_dir", env.Dir("gdextension").abspath), env)
  304. api_file = normalize_path(env.get("custom_api_file", env.File(extension_dir + "/extension_api.json").abspath), env)
  305. bindings = env.GodotCPPBindings(
  306. env.Dir("."),
  307. [
  308. api_file,
  309. os.path.join(extension_dir, "gdextension_interface.h"),
  310. "binding_generator.py",
  311. ],
  312. )
  313. # Forces bindings regeneration.
  314. if env["generate_bindings"]:
  315. env.AlwaysBuild(bindings)
  316. env.NoCache(bindings)
  317. # Sources to compile
  318. sources = []
  319. add_sources(sources, "src", "cpp")
  320. add_sources(sources, "src/classes", "cpp")
  321. add_sources(sources, "src/core", "cpp")
  322. add_sources(sources, "src/variant", "cpp")
  323. sources.extend([f for f in bindings if str(f).endswith(".cpp")])
  324. # Includes
  325. env.AppendUnique(CPPPATH=[env.Dir(d) for d in [extension_dir, "include", "gen/include"]])
  326. library = None
  327. library_name = "libgodot-cpp" + env["suffix"] + env["LIBSUFFIX"]
  328. if env["build_library"]:
  329. library = env.StaticLibrary(target=env.File("bin/%s" % library_name), source=sources)
  330. default_args = [library]
  331. # Add compiledb if the option is set
  332. if env.get("compiledb", False):
  333. default_args += ["compiledb"]
  334. env.Default(*default_args)
  335. env.AppendUnique(LIBS=[env.File("bin/%s" % library_name)])
  336. return library