#!/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=") 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 Pandemonium headers", "pandemonium_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 pandemonium-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"], "gen", "core"]]]) # Sources to compile sources = [] add_sources(sources, "core", "cpp") add_sources(sources, "core/os", "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 = "libpandemonium-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")