gdnative_cpp/SConstruct

526 lines
18 KiB
Python

#!/usr/bin/env python
import os
import sys
import subprocess
from binding_generator import scons_generate_bindings, scons_emit_files
if sys.version_info < (3,):
def decode_utf8(x):
return x
else:
import codecs
def decode_utf8(x):
return codecs.utf_8_decode(x)[0]
# Workaround for MinGW. See:
# http://www.scons.org/wiki/LongCmdLinesOnWin32
if os.name == "nt":
import subprocess
def mySubProcess(cmdline, env):
# print "SPAWNED : " + cmdline
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
proc = subprocess.Popen(
cmdline,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
startupinfo=startupinfo,
shell=False,
env=env,
)
data, err = proc.communicate()
rv = proc.wait()
if rv:
print("=====")
print(err.decode("utf-8"))
print("=====")
return rv
def mySpawn(sh, escape, cmd, args, env):
newargs = " ".join(args[1:])
cmdline = cmd + " " + newargs
rv = 0
if len(cmdline) > 32000 and cmd.endswith("ar"):
cmdline = cmd + " " + args[1] + " " + args[2] + " "
for i in range(3, len(args)):
rv = mySubProcess(cmdline + args[i], env)
if rv:
break
else:
rv = mySubProcess(cmdline, env)
return rv
def add_sources(sources, dir, extension):
for f in os.listdir(dir):
if f.endswith("." + extension):
sources.append(dir + "/" + f)
# Try to detect the host platform automatically.
# This is used if no `platform` argument is passed
if sys.platform.startswith("linux"):
host_platform = "linux"
elif sys.platform.startswith("freebsd"):
host_platform = "freebsd"
elif sys.platform == "darwin":
host_platform = "osx"
elif sys.platform == "win32" or sys.platform == "msys":
host_platform = "windows"
else:
raise ValueError("Could not detect platform automatically, please specify with platform=<platform>")
env = Environment(ENV=os.environ)
# 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)
# os.cpu_count() requires Python 3.4+.
if hasattr(os, "cpu_count") and env.GetOption("num_jobs") == altered_num_jobs:
cpu_count = os.cpu_count()
if cpu_count is None:
print("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
print(
"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)
is64 = sys.maxsize > 2 ** 32
if (
env["TARGET_ARCH"] == "amd64"
or env["TARGET_ARCH"] == "emt64"
or env["TARGET_ARCH"] == "x86_64"
or env["TARGET_ARCH"] == "arm64-v8a"
):
is64 = True
opts = Variables([], ARGUMENTS)
opts.Add(
EnumVariable(
"platform",
"Target platform",
host_platform,
allowed_values=("linux", "freebsd", "osx", "windows", "android", "ios", "javascript"),
ignorecase=2,
)
)
opts.Add(EnumVariable("bits", "Target platform bits", "64" if is64 else "32", ("32", "64")))
opts.Add(BoolVariable("use_llvm", "Use the LLVM compiler - only effective when targeting Linux or FreeBSD", False))
opts.Add(BoolVariable("use_mingw", "Use the MinGW compiler instead of MSVC - only effective on Windows", False))
# Must be the same setting as used for cpp_bindings
opts.Add(EnumVariable("target", "Compilation target", "debug", allowed_values=("debug", "release"), ignorecase=2))
opts.Add(
PathVariable(
"headers_dir",
"Path to the directory containing Godot headers",
"godot-headers",
PathVariable.PathIsDir,
)
)
opts.Add(PathVariable("custom_api_file", "Path to a custom JSON API file", None, PathVariable.PathIsFile))
opts.Add(BoolVariable("generate_bindings", "Force GDNative API bindings generation.", False))
opts.Add(
EnumVariable(
"android_arch",
"Target Android architecture",
"armv7",
["armv7", "arm64v8", "x86", "x86_64"],
)
)
opts.Add("macos_deployment_target", "macOS deployment target", "default")
opts.Add("macos_sdk_path", "macOS SDK path", "")
opts.Add(EnumVariable("macos_arch", "Target macOS architecture", "universal", ["universal", "x86_64", "arm64"]))
opts.Add(EnumVariable("ios_arch", "Target iOS architecture", "arm64", ["armv7", "arm64", "x86_64"]))
opts.Add(BoolVariable("ios_simulator", "Target iOS Simulator", False))
opts.Add(
"IPHONEPATH",
"Path to iPhone toolchain",
"/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain",
)
opts.Add(
"android_api_level",
"Target Android API level",
"18" if ARGUMENTS.get("android_arch", "armv7") in ["armv7", "x86"] else "21",
)
opts.Add(
"ANDROID_NDK_ROOT",
"Path to your Android NDK installation. By default, uses ANDROID_NDK_ROOT from your defined environment variables.",
os.environ.get("ANDROID_NDK_ROOT", None),
)
opts.Add(
BoolVariable(
"generate_template_get_node",
"Generate a template version of the Node class's get_node.",
True,
)
)
opts.Add(BoolVariable("build_library", "Build the godot-cpp library.", True))
opts.Update(env)
Help(opts.GenerateHelpText(env))
# 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])
# This makes sure to keep the session environment variables on Windows.
# This way, you can run SCons in a Visual Studio 2017 prompt and it will find
# all the required tools
if host_platform == "windows" and env["platform"] != "android":
if env["bits"] == "64":
env = Environment(TARGET_ARCH="amd64")
elif env["bits"] == "32":
env = Environment(TARGET_ARCH="x86")
opts.Update(env)
# Require C++14
if host_platform == "windows" and env["platform"] == "windows" and not env["use_mingw"]:
# MSVC
env.Append(CCFLAGS=["/std:c++14"])
else:
env.Append(CCFLAGS=["-std=c++14"])
if env["platform"] == "linux" or env["platform"] == "freebsd":
if env["use_llvm"]:
env["CXX"] = "clang++"
env.Append(CCFLAGS=["-fPIC", "-Wwrite-strings"])
env.Append(LINKFLAGS=["-Wl,-R,'$$ORIGIN'"])
if env["target"] == "debug":
env.Append(CCFLAGS=["-Og", "-g"])
elif env["target"] == "release":
env.Append(CCFLAGS=["-O3"])
if env["bits"] == "64":
env.Append(CCFLAGS=["-m64"])
env.Append(LINKFLAGS=["-m64"])
elif env["bits"] == "32":
env.Append(CCFLAGS=["-m32"])
env.Append(LINKFLAGS=["-m32"])
elif env["platform"] == "osx":
# Use Clang on macOS by default
env["CXX"] = "clang++"
if env["bits"] == "32":
raise ValueError("Only 64-bit builds are supported for the macOS target.")
if env["macos_arch"] == "universal":
env.Append(LINKFLAGS=["-arch", "x86_64", "-arch", "arm64"])
env.Append(CCFLAGS=["-arch", "x86_64", "-arch", "arm64"])
else:
env.Append(LINKFLAGS=["-arch", env["macos_arch"]])
env.Append(CCFLAGS=["-arch", env["macos_arch"]])
if env["macos_deployment_target"] != "default":
env.Append(CCFLAGS=["-mmacosx-version-min=" + env["macos_deployment_target"]])
env.Append(LINKFLAGS=["-mmacosx-version-min=" + env["macos_deployment_target"]])
if env["macos_sdk_path"]:
env.Append(CCFLAGS=["-isysroot", env["macos_sdk_path"]])
env.Append(LINKFLAGS=["-isysroot", env["macos_sdk_path"]])
env.Append(LINKFLAGS=["-Wl,-undefined,dynamic_lookup"])
if env["target"] == "debug":
env.Append(CCFLAGS=["-Og", "-g"])
elif env["target"] == "release":
env.Append(CCFLAGS=["-O3"])
elif env["platform"] == "ios":
if env["ios_simulator"]:
sdk_name = "iphonesimulator"
env.Append(CCFLAGS=["-mios-simulator-version-min=10.0"])
else:
sdk_name = "iphoneos"
env.Append(CCFLAGS=["-miphoneos-version-min=10.0"])
try:
sdk_path = decode_utf8(subprocess.check_output(["xcrun", "--sdk", sdk_name, "--show-sdk-path"]).strip())
except (subprocess.CalledProcessError, OSError):
raise ValueError("Failed to find SDK path while running xcrun --sdk {} --show-sdk-path.".format(sdk_name))
compiler_path = env["IPHONEPATH"] + "/usr/bin/"
env["ENV"]["PATH"] = env["IPHONEPATH"] + "/Developer/usr/bin/:" + env["ENV"]["PATH"]
env["CC"] = compiler_path + "clang"
env["CXX"] = compiler_path + "clang++"
env["AR"] = compiler_path + "ar"
env["RANLIB"] = compiler_path + "ranlib"
env["SHLIBSUFFIX"] = ".dylib"
env.Append(CCFLAGS=["-arch", env["ios_arch"], "-isysroot", sdk_path])
env.Append(
LINKFLAGS=[
"-arch",
env["ios_arch"],
"-Wl,-undefined,dynamic_lookup",
"-isysroot",
sdk_path,
"-F" + sdk_path,
]
)
if env["target"] == "debug":
env.Append(CCFLAGS=["-Og", "-g"])
elif env["target"] == "release":
env.Append(CCFLAGS=["-O3"])
elif env["platform"] == "windows":
if host_platform == "windows" and not env["use_mingw"]:
# MSVC
env.Append(LINKFLAGS=["/WX"])
if env["target"] == "debug":
env.Append(CCFLAGS=["/Z7", "/Od", "/EHsc", "/D_DEBUG", "/MDd"])
elif env["target"] == "release":
env.Append(CCFLAGS=["/O2", "/EHsc", "/DNDEBUG", "/MD"])
elif host_platform == "linux" or host_platform == "freebsd" or host_platform == "osx":
# Cross-compilation using MinGW
if env["bits"] == "64":
env["CXX"] = "x86_64-w64-mingw32-g++"
env["AR"] = "x86_64-w64-mingw32-ar"
env["RANLIB"] = "x86_64-w64-mingw32-ranlib"
env["LINK"] = "x86_64-w64-mingw32-g++"
elif env["bits"] == "32":
env["CXX"] = "i686-w64-mingw32-g++"
env["AR"] = "i686-w64-mingw32-ar"
env["RANLIB"] = "i686-w64-mingw32-ranlib"
env["LINK"] = "i686-w64-mingw32-g++"
elif host_platform == "windows" and env["use_mingw"]:
# Don't Clone the environment. Because otherwise, SCons will pick up msvc stuff.
env = Environment(ENV=os.environ, tools=["mingw"])
opts.Update(env)
# Still need to use C++14.
env.Append(CCFLAGS=["-std=c++14"])
# Don't want lib prefixes
env["IMPLIBPREFIX"] = ""
env["SHLIBPREFIX"] = ""
env["SPAWN"] = mySpawn
env.Replace(ARFLAGS=["q"])
# Native or cross-compilation using MinGW
if host_platform == "linux" or host_platform == "freebsd" or host_platform == "osx" or env["use_mingw"]:
# These options are for a release build even using target=debug
env.Append(CCFLAGS=["-O3", "-Wwrite-strings"])
env.Append(
LINKFLAGS=[
"--static",
"-Wl,--no-undefined",
"-static-libgcc",
"-static-libstdc++",
]
)
elif env["platform"] == "android":
if host_platform == "windows":
# Don't Clone the environment. Because otherwise, SCons will pick up msvc stuff.
env = Environment(ENV=os.environ, tools=["mingw"])
opts.Update(env)
# Long line hack. Use custom spawn, quick AR append (to avoid files with the same names to override each other).
env["SPAWN"] = mySpawn
env.Replace(ARFLAGS=["q"])
# Verify NDK root
if not "ANDROID_NDK_ROOT" in env:
raise ValueError(
"To build for Android, ANDROID_NDK_ROOT must be defined. Please set ANDROID_NDK_ROOT to the root folder of your Android NDK installation."
)
# Validate API level
api_level = int(env["android_api_level"])
if env["android_arch"] in ["x86_64", "arm64v8"] and api_level < 21:
print("WARN: 64-bit Android architectures require an API level of at least 21; setting android_api_level=21")
env["android_api_level"] = "21"
api_level = 21
# Setup toolchain
toolchain = env["ANDROID_NDK_ROOT"] + "/toolchains/llvm/prebuilt/"
if host_platform == "windows":
toolchain += "windows"
import platform as pltfm
if pltfm.machine().endswith("64"):
toolchain += "-x86_64"
elif host_platform == "linux":
toolchain += "linux-x86_64"
elif host_platform == "osx":
toolchain += "darwin-x86_64"
env.PrependENVPath("PATH", toolchain + "/bin") # This does nothing half of the time, but we'll put it here anyways
# Get architecture info
arch_info_table = {
"armv7": {
"march": "armv7-a",
"target": "armv7a-linux-androideabi",
"tool_path": "arm-linux-androideabi",
"compiler_path": "armv7a-linux-androideabi",
"ccflags": ["-mfpu=neon"],
},
"arm64v8": {
"march": "armv8-a",
"target": "aarch64-linux-android",
"tool_path": "aarch64-linux-android",
"compiler_path": "aarch64-linux-android",
"ccflags": [],
},
"x86": {
"march": "i686",
"target": "i686-linux-android",
"tool_path": "i686-linux-android",
"compiler_path": "i686-linux-android",
"ccflags": ["-mstackrealign"],
},
"x86_64": {
"march": "x86-64",
"target": "x86_64-linux-android",
"tool_path": "x86_64-linux-android",
"compiler_path": "x86_64-linux-android",
"ccflags": [],
},
}
arch_info = arch_info_table[env["android_arch"]]
# Setup tools
env["CC"] = toolchain + "/bin/clang"
env["CXX"] = toolchain + "/bin/clang++"
env["AR"] = toolchain + "/bin/llvm-ar"
env["AS"] = toolchain + "/bin/llvm-as"
env["LD"] = toolchain + "/bin/llvm-ld"
env["STRIP"] = toolchain + "/bin/llvm-strip"
env["RANLIB"] = toolchain + "/bin/llvm-ranlib"
env["SHLIBSUFFIX"] = ".so"
env.Append(
CCFLAGS=[
"--target=" + arch_info["target"] + env["android_api_level"],
"-march=" + arch_info["march"],
"-fPIC",
]
)
env.Append(CCFLAGS=arch_info["ccflags"])
env.Append(LINKFLAGS=["--target=" + arch_info["target"] + env["android_api_level"], "-march=" + arch_info["march"]])
if env["target"] == "debug":
env.Append(CCFLAGS=["-Og", "-g"])
elif env["target"] == "release":
env.Append(CCFLAGS=["-O3"])
elif env["platform"] == "javascript":
env["ENV"] = os.environ
env["CC"] = "emcc"
env["CXX"] = "em++"
env["AR"] = "emar"
env["RANLIB"] = "emranlib"
env.Append(CPPFLAGS=["-s", "SIDE_MODULE=1"])
env.Append(LINKFLAGS=["-s", "SIDE_MODULE=1"])
env["SHOBJSUFFIX"] = ".bc"
env["SHLIBSUFFIX"] = ".wasm"
# Use TempFileMunge since some AR invocations are too long for cmd.exe.
# Use POSIX-style paths, required with TempFileMunge.
env["ARCOM_POSIX"] = env["ARCOM"].replace("$TARGET", "$TARGET.posix").replace("$SOURCES", "$SOURCES.posix")
env["ARCOM"] = "${TEMPFILE(ARCOM_POSIX)}"
# All intermediate files are just LLVM bitcode.
env["OBJPREFIX"] = ""
env["OBJSUFFIX"] = ".bc"
env["PROGPREFIX"] = ""
# Program() output consists of multiple files, so specify suffixes manually at builder.
env["PROGSUFFIX"] = ""
env["LIBPREFIX"] = "lib"
env["LIBSUFFIX"] = ".a"
env["LIBPREFIXES"] = ["$LIBPREFIX"]
env["LIBSUFFIXES"] = ["$LIBSUFFIX"]
env.Replace(SHLINKFLAGS="$LINKFLAGS")
env.Replace(SHLINKFLAGS="$LINKFLAGS")
if env["target"] == "debug":
env.Append(CCFLAGS=["-O0", "-g"])
elif env["target"] == "release":
env.Append(CCFLAGS=["-O3"])
# Cache
scons_cache_path = os.environ.get("SCONS_CACHE")
if scons_cache_path is not None:
CacheDir(scons_cache_path)
Decider("MD5")
# Generate bindings
env.Append(BUILDERS={"GenerateBindings": Builder(action=scons_generate_bindings, emitter=scons_emit_files)})
json_api_file = ""
if "custom_api_file" in env:
json_api_file = env["custom_api_file"]
else:
json_api_file = os.path.join(os.getcwd(), env["headers_dir"], "api.json")
bindings = env.GenerateBindings(
env.Dir("."), [json_api_file, "binding_generator.py"]
)
# Forces bindings regeneration.
if env["generate_bindings"]:
AlwaysBuild(bindings)
NoCache(bindings)
# Includes
env.Append(CPPPATH=[[env.Dir(d) for d in [".", env["headers_dir"], "include", "include/gen", "include/core"]]])
# Sources to compile
sources = []
add_sources(sources, "src/core", "cpp")
sources.extend(f for f in bindings if str(f).endswith(".cpp"))
arch_suffix = env["bits"]
if env["platform"] == "android":
arch_suffix = env["android_arch"]
elif env["platform"] == "ios":
arch_suffix = env["ios_arch"]
if env["ios_simulator"]:
arch_suffix += ".simulator"
elif env["platform"] == "osx":
if env["macos_arch"] != "universal":
arch_suffix = env["macos_arch"]
elif env["platform"] == "javascript":
arch_suffix = "wasm"
# Expose it to projects that import this env.
env["arch_suffix"] = arch_suffix
library = None
env["OBJSUFFIX"] = ".{}.{}.{}{}".format(env["platform"], env["target"], arch_suffix, env["OBJSUFFIX"])
library_name = "libgodot-cpp.{}.{}.{}{}".format(env["platform"], env["target"], arch_suffix, env["LIBSUFFIX"])
if env["build_library"]:
library = env.StaticLibrary(target=env.File("bin/%s" % library_name), source=sources)
Default(library)
env.Append(LIBPATH=[env.Dir("bin")])
env.Append(LIBS=library_name)
Return("env")