Skip to content

Instantly share code, notes, and snippets.

@Ed94
Last active February 23, 2023 22:46
Show Gist options
  • Select an option

  • Save Ed94/aaf44828605878ca14f1eebab4af67ee to your computer and use it in GitHub Desktop.

Select an option

Save Ed94/aaf44828605878ca14f1eebab4af67ee to your computer and use it in GitHub Desktop.
godot-cpp multi-module fast incremental compile sconstruct example
#!/rusr/bin/env python
# Py Platform
import os
import platform
import shutil
import subprocess
import sys
import time
CacheDir('./build_cache')
DefaultEnvironment(tools=[])
# The following is an SConstruct designed to build all the compiled modules for Sectored Language project.
# It assumes that godot_cpp has already been built.
#region Helper Functions
# add_sources( sources, SUB-DIRECTORY-NAME, "c or cpp" )
def add_sources(sources, dir, extension):
for file in os.listdir(dir):
if file.endswith("." + extension):
sources.append(dir + "/" + file)
def no_verbose(sys, env):
colors = {}
# Colors are disabled in non-TTY environments such as pipes. This means
# that if output is redirected to a file, it will not contain color codes
if sys.stdout.isatty():
colors["blue"] = "\033[0;94m"
colors["bold_blue"] = "\033[1;94m"
colors["reset"] = "\033[0m"
else:
colors["blue"] = ""
colors["bold_blue"] = ""
colors["reset"] = ""
# There is a space before "..." to ensure that source file names can be
# Ctrl + clicked in the VS Code terminal.
compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(
colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
)
java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(
colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
)
compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(
colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
)
link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(
colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
)
link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(
colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
)
ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(
colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
)
link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(
colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
)
java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(
colors["blue"], colors["bold_blue"], colors["blue"], colors["reset"]
)
env.Append(CXXCOMSTR=[compile_source_message])
env.Append(CCCOMSTR=[compile_source_message])
env.Append(SHCCCOMSTR=[compile_shared_source_message])
env.Append(SHCXXCOMSTR=[compile_shared_source_message])
env.Append(ARCOMSTR=[link_library_message])
env.Append(RANLIBCOMSTR=[ranlib_library_message])
env.Append(SHLINKCOMSTR=[link_shared_library_message])
env.Append(LINKCOMSTR=[link_program_message])
env.Append(JARCOMSTR=[java_library_message])
env.Append(JAVACCOMSTR=[java_compile_source_message])
start_time = round( time.time() * 1000 )
def time_mili():
return round( time.time() * 1000 - start_time )
def plog( msg ):
print( '{0:10} ms {1}'.format( time_mili() , msg ) )
#endregion Helper Functions
#region Module Paths
# Module name assuemd to be directory name, change if undesired.
# module_name = os.path.basename(os.getcwd())
# plog( "Building module: " + module_name )
module_core = "Core"
module_engine = "Engine"
module_pmdb = "PMDB"
module_project = "Project"
module_simpleregex = "SimpleRegex"
module_textpipeline = "TextPipeline"
module_ux = "UX"
# This is the root directory of your project
# This template assumes the following directories
# Godot project
# Platform (godot, godot_cpp, and other platform/third-party specific source/binaries)
# Source (Native gdextension source modules for the godot project)
# Change these based on your project
path_root = os.path.abspath("../")
path_gdproject = path_root + "/App"
path_platform = path_root + "/Platform"
path_gdengine = path_platform + "/gd"
path_godotcpp = path_platform + "/gd-cpp"
path_source = path_root + "/Source"
path_module_core = path_source + "/" + module_core
path_module_engine = path_source + "/" + module_engine
path_module_pmdb = path_source + "/" + module_pmdb
path_module_project = path_source + "/" + module_project
path_module_simpleregex = path_source + "/" + module_simpleregex
path_module_textpipeline = path_source + "/" + module_textpipeline
path_module_ux = path_source + "/" + module_ux
#endregion Module Paths
#region GDExtension Scons Configuration
#region Default Platform
env_tools = \
[]
# [ "default" ]
# Try to detect the host platform automatically.
# This is used if no `platform` argument is passed
if sys.platform.startswith("linux"):
default_platform = "linux"
env_tools = [ 'g++', 'ilink' ]
elif sys.platform == "darwin":
default_platform = "macos"
env_tools = [ 'clang', 'applelink' ]
elif sys.platform == "win32" or sys.platform == "msys":
default_platform = "windows"
env_tools = [ 'msvc', 'mslink' ]
elif ARGUMENTS.get("platform", ""):
default_platform = ARGUMENTS.get("platform")
else:
raise ValueError("Could not detect platform automatically, please specify with platform=<platform>")
plog( "Default Platform: " + default_platform)
#endregion Default Platform
env = Environment( tools = env_tools )
plog( "Created Enviornment" )
# env['ENV']['TERM'] = os.environ['TERM']
#region Job Execution
# Default num_jobs to local cpu count if not user specified.
# SCons has a peculiarity where user-specified options won't be overridden
# by SetOption, so we can rely on this to know if we should use our default.
initial_num_jobs = env.GetOption("num_jobs")
altered_num_jobs = initial_num_jobs + 1
env.SetOption("num_jobs", altered_num_jobs)
if env.GetOption("num_jobs") == altered_num_jobs:
cpu_count = os.cpu_count()
if cpu_count is None:
plog("Couldn't auto-detect CPU count to configure build parallelism. Specify it with the -j argument.")
else:
safer_cpu_count = cpu_count if cpu_count <= 4 else cpu_count - 1
plog(
"Auto-detected %d CPU cores available for build parallelism. Using %d cores by default. You can override it with the -j argument."
% (cpu_count, safer_cpu_count)
)
env.SetOption("num_jobs", safer_cpu_count)
#endregion Job Execution
#region Custom options and profile flags.
customs = ["custom.py"]
profile = ARGUMENTS.get("profile", "")
if profile:
if os.path.isfile(profile):
customs.append(profile)
elif os.path.isfile(profile + ".py"):
customs.append(profile + ".py")
opts = Variables(customs, ARGUMENTS)
platforms = ("linux", "macos", "windows", "android", "ios", "javascript")
opts.Add(
EnumVariable(
"platform",
"Target platform",
default_platform,
allowed_values=platforms,
ignorecase=2,
)
)
# Editor and template_debug are compatible (i.e. you can use the same binary for Godot editor builds and Godot debug templates).
# Godot release templates are only compatible with "template_release" builds.
# For this reason, we default to template_debug builds, unlike Godot which defaults to editor builds.
opts.Add( EnumVariable("target", "Compilation target", "template_debug", ("editor", "template_release", "template_debug")) )
opts.Add( PathVariable("gdextension_dir", "Path to the directory containing GDExtension interface header and API JSON file",
path_godotcpp + "/gdextension",
PathVariable.PathIsDir,
)
)
opts.Add( PathVariable("custom_api_file", "Path to a custom JSON API file", None, PathVariable.PathIsFile))
opts.Add( EnumVariable("float", "Floating-point precision", "32", ("32", "64")))
# CPU architecture options.
architecture_array = ["", "universal", "x86_32", "x86_64", "arm32", "arm64", "rv64", "ppc32", "ppc64", "wasm32"]
architecture_aliases = {
"x64": "x86_64",
"amd64": "x86_64",
"armv7": "arm32",
"armv8": "arm64",
"arm64v8": "arm64",
"aarch64": "arm64",
"rv": "rv64",
"riscv": "rv64",
"riscv64": "rv64",
"ppcle": "ppc32",
"ppc": "ppc32",
"ppc64le": "ppc64",
}
opts.Add(EnumVariable("arch", "CPU architecture", "", architecture_array, architecture_aliases))
# Add platform options
tools = {}
path_godotcpp_tools = path_godotcpp + "/tools"
for pl in platforms:
tool = Tool( pl, toolpath = [ path_godotcpp_tools ] )
if hasattr(tool, "options"):
tool.options(opts)
tools[pl] = tool
plog( "Populated platform tools")
# Targets flags tool (optimizations, debug symbols)
target_tool = Tool( "targets", toolpath = [ path_godotcpp_tools ] )
target_tool.options(opts)
opts.Update(env)
# Disable this if doing incrementals
# Help(opts.GenerateHelpText(env))
plog( "Build Options updated" )
#endregion Custom options and profile flags.
#region Toolchain generation
# Process CPU architecture argument.
if env["arch"] == "":
# No architecture specified. Default to arm64 if building for Android,
# universal if building for macOS or iOS, wasm32 if building for web,
# otherwise default to the host architecture.
if env["platform"] in ["macos", "ios"]:
env["arch"] = "universal"
elif env["platform"] == "android":
env["arch"] = "arm64"
elif env["platform"] == "javascript":
env["arch"] = "wasm32"
else:
host_machine = platform.machine().lower()
if host_machine in architecture_array:
env["arch"] = host_machine
elif host_machine in architecture_aliases.keys():
env["arch"] = architecture_aliases[host_machine]
elif "86" in host_machine:
# Catches x86, i386, i486, i586, i686, etc.
env["arch"] = "x86_32"
else:
print("Unsupported CPU architecture: " + host_machine)
Exit()
tool = Tool( env["platform"], toolpath = [ path_godotcpp_tools ] )
if tool is None or not tool.exists(env):
raise ValueError("Required toolchain not found for platform " + env["platform"])
tool.generate(env)
target_tool.generate(env)
#endregion Toolchain generation
# Detect and print a warning listing unknown SCons variables to ease troubleshooting.
unknown = opts.UnknownVariables()
if unknown:
print("WARNING: Unknown SCons variables were passed and will be ignored:")
for item in unknown.items():
print(" " + item[0] + "=" + item[1])
plog("Building for architecture " + env["arch"] + " on platform " + env["platform"])
# Require C++17
if env.get("is_msvc", False):
env.Append(CXXFLAGS=["/std:c++17"])
else:
env.Append(CXXFLAGS=["-std=c++17"])
if env["float"] == "64":
env.Append(CPPDEFINES=["REAL_T_IS_DOUBLE"])
scons_cache_path = os.environ.get("SCONS_CACHE")
if scons_cache_path is not None:
CacheDir(scons_cache_path)
Decider("MD5")
plog( "scons_cache_path: " + scons_cache_path )
suffix = ".{}.{}".format(env["platform"], env["target"])
if env.dev_build:
suffix += ".dev"
if env["float"] == "64":
suffix += ".double"
suffix += "." + env["arch"]
if env["ios_simulator"]:
suffix += ".simulator"
# Expose it when included from another project
env["suffix"] = suffix
env["OBJSUFFIX"] = suffix + env["OBJSUFFIX"]
if not env.get("verbose"):
no_verbose(sys, env)
#endregion GDExtension Scons Configuration
#region GDExtesnion Library Configuration
# GODOT CPP GDEXTENSION INCLUDES
include_godot_cpp_directories = [
path_godotcpp + "/gdextension",
path_godotcpp + "/include",
path_godotcpp + "/gen/include",
]
env.Append( CPPPATH = include_godot_cpp_directories )
path_libgodot = path_godotcpp + "/bin"
libgodot = "libgodot-cpp{}{}".format(suffix, env["LIBSUFFIX"])
env.Append( LIBPATH = path_libgodot)
env.Append( LIBS = libgodot)
plog( "GDExtension library configured")
#endregion GDExtesnion Library Configuration
#region Module Configuration Helpers
library_targetsig = "{}".format( suffix )
library_suffixlib = "{}".format( suffix, env["LIBSUFFIX"] )
library_suffixshared = "{}".format( suffix, env["SHLIBSUFFIX"] )
def define_shared_library( build_env, module_name, module_sources):
if build_env["platform"] == "macos":
return build_env.SharedLibrary(
path_gdproject + "/Extensions/bin/" + module_name + "{}.{}.framework/" + module_name + ".{}.{}".format(
build_env["platform"], build_env["target"], build_env["platform"], build_env["target"]
),
source=module_sources,
)
else:
return build_env.SharedLibrary(
path_gdproject + "/Extensions/bin/" + module_name + "{}{}".format(env["suffix"], build_env["SHLIBSUFFIX"]),
source=module_sources,
)
def define_symbol_file( build_env, module_name ):
if build_env["platform"] == "windows":
return module_name + library_targetsig + ".pdb"
#endregion
#region Module Configuration
# GENERAL INCLUDES
include_directories = [
path_platform,
path_godotcpp,
path_source,
]
env.Append( CPPPATH = include_directories )
# GENERAL LIBRARIES
library_directories = [
path_gdproject + "/Extensions/bin",
]
env.Append( LIBPATH = library_directories )
env_core = env.Clone()
env_engine = env.Clone()
env_simpleregex = env.Clone()
env_textpipeline = env.Clone()
env_core.Append( CPPPATH = [ path_module_core ] )
env_engine.Append( CPPPATH = [ path_module_engine ] )
env_simpleregex.Append( CPPPATH = [ path_module_simpleregex ] )
env_textpipeline.Append( CPPPATH = [ path_module_simpleregex ] )
libaries_core = [
]
libraries_engine = [
"Core" + library_suffixlib,
"TextPipeline" + library_suffixlib,
]
libraries_simpleregex = [
"Core" + library_suffixlib,
]
libraries_textpipeline = [
"Core" + library_suffixlib,
"SimpleRegex" + library_suffixlib,
]
env_core.Append( LIBS = libaries_core )
env_engine.Append( LIBS = libraries_engine )
env_simpleregex.Append( LIBS = libraries_simpleregex )
env_textpipeline.Append( LIBS = libraries_textpipeline )
sources_core = []
sources_engine = []
sources_simpleregex = []
sources_textpipeline = []
add_sources( sources_core, "./" + module_core, "cpp" )
add_sources( sources_engine, "./" + module_engine, "cpp")
add_sources( sources_simpleregex, "./" + module_simpleregex, "cpp")
add_sources( sources_textpipeline, "./" + module_textpipeline, "cpp")
# These are lists of files to generate for the env.
library_core = define_shared_library( env_core, module_core, sources_core)
library_engine = define_shared_library( env_engine, module_engine, sources_engine)
library_simpleregex = define_shared_library( env_simpleregex, module_simpleregex, sources_simpleregex)
library_textpipeline = define_shared_library( env_textpipeline, module_textpipeline, sources_textpipeline)
plog("Module configured")
#endregion Module Configuration
#region Build Configuraiton
env_simpleregex.Depends( library_simpleregex, library_core )
env_textpipeline.Depends( library_textpipeline, library_simpleregex )
env_engine.Depends( library_engine, library_textpipeline )
env_core.Default( library_core )
env_engine.Default( library_engine )
env_simpleregex.Default( library_simpleregex )
env_textpipeline.Default( library_textpipeline )
#endregion Building
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment