diff --git a/compat.py b/compat.py new file mode 100644 index 0000000..8fcaa4f --- /dev/null +++ b/compat.py @@ -0,0 +1,97 @@ +import sys + +if sys.version_info < (3,): + + def isbasestring(s): + return isinstance(s, basestring) + + def open_utf8(filename, mode): + return open(filename, mode) + + def byte_to_str(x): + return str(ord(x)) + + import cStringIO + + def StringIO(): + return cStringIO.StringIO() + + def encode_utf8(x): + return x + + def decode_utf8(x): + return x + + def iteritems(d): + return d.iteritems() + + def itervalues(d): + return d.itervalues() + + def escape_string(s): + if isinstance(s, unicode): + s = s.encode("ascii") + result = "" + for c in s: + if not (32 <= ord(c) < 127) or c in ("\\", '"'): + result += "\\%03o" % ord(c) + else: + result += c + return result + + def qualname(obj): + # Not properly equivalent to __qualname__ in py3, but it doesn't matter. + return obj.__name__ + + +else: + + def isbasestring(s): + return isinstance(s, (str, bytes)) + + def open_utf8(filename, mode): + return open(filename, mode, encoding="utf-8") + + def byte_to_str(x): + return str(x) + + import io + + def StringIO(): + return io.StringIO() + + import codecs + + def encode_utf8(x): + return codecs.utf_8_encode(x)[0] + + def decode_utf8(x): + return codecs.utf_8_decode(x)[0] + + def iteritems(d): + return iter(d.items()) + + def itervalues(d): + return iter(d.values()) + + def charcode_to_c_escapes(c): + rev_result = [] + while c >= 256: + c, low = (c // 256, c % 256) + rev_result.append("\\%03o" % low) + rev_result.append("\\%03o" % c) + return "".join(reversed(rev_result)) + + def escape_string(s): + result = "" + if isinstance(s, str): + s = s.encode("utf-8") + for c in s: + if not (32 <= c < 127) or c in (ord("\\"), ord('"')): + result += charcode_to_c_escapes(c) + else: + result += chr(c) + return result + + def qualname(obj): + return obj.__qualname__ diff --git a/platform/SCsub b/platform/SCsub new file mode 100644 index 0000000..469a57e --- /dev/null +++ b/platform/SCsub @@ -0,0 +1,12 @@ +#!/usr/bin/env python + +from compat import open_utf8 + +Import("env") + +env.platform_sources = [] + +env.add_source_files(env.platform_sources, "sfwl.cpp") + +lib = env.add_library("platform", env.platform_sources) +env.Prepend(LIBS=[lib]) diff --git a/platform/osx/detect.py b/platform/osx/detect.py new file mode 100644 index 0000000..9526654 --- /dev/null +++ b/platform/osx/detect.py @@ -0,0 +1,220 @@ +import os +import sys +from methods import detect_darwin_sdk_path, get_compiler_version, is_vanilla_clang + + +def is_active(): + return True + + +def get_name(): + return "OSX" + + +def can_build(): + if sys.platform == "darwin" or ("OSXCROSS_ROOT" in os.environ): + return True + + return False + + +def get_opts(): + from SCons.Variables import BoolVariable, EnumVariable + + return [ + ("osxcross_sdk", "OSXCross SDK version", "darwin14"), + ("MACOS_SDK_PATH", "Path to the macOS SDK", ""), + EnumVariable("macports_clang", "Build using Clang from MacPorts", "no", ("no", "5.0", "devel")), + BoolVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", True), + BoolVariable("separate_debug_symbols", "Create a separate file containing debugging symbols", False), + BoolVariable("use_ubsan", "Use LLVM/GCC compiler undefined behavior sanitizer (UBSAN)", False), + BoolVariable("use_asan", "Use LLVM/GCC compiler address sanitizer (ASAN))", False), + BoolVariable("use_lsan", "Use LLVM/GCC compiler leak sanitizer (LSAN))", False), + BoolVariable("use_tsan", "Use LLVM/GCC compiler thread sanitizer (TSAN))", False), + ] + + +def get_flags(): + return [] + + +def configure(env): + ## Build type + + if env["target"] == "release": + if env["optimize"] == "speed": # optimize for speed (default) + env.Prepend(CCFLAGS=["-O3"]) + elif env["optimize"] == "size": # optimize for size + env.Prepend(CCFLAGS=["-Os"]) + if env["arch"] != "arm64": + env.Prepend(CCFLAGS=["-msse2"]) + + if env["debug_symbols"]: + env.Prepend(CCFLAGS=["-g2"]) + + elif env["target"] == "release_debug": + if env["optimize"] == "speed": # optimize for speed (default) + env.Prepend(CCFLAGS=["-O2"]) + elif env["optimize"] == "size": # optimize for size + env.Prepend(CCFLAGS=["-Os"]) + + if env["debug_symbols"]: + env.Prepend(CCFLAGS=["-g2"]) + + elif env["target"] == "debug": + env.Prepend(CCFLAGS=["-g3"]) + env.Prepend(LINKFLAGS=["-Xlinker", "-no_deduplicate"]) + + ## Architecture + + # Mac OS X no longer runs on 32-bit since 10.7 which is unsupported since 2014 + # As such, we only support 64-bit + env["bits"] = "64" + + ## Compiler configuration + + # Save this in environment for use by other modules + if "OSXCROSS_ROOT" in os.environ: + env["osxcross"] = True + + if env["arch"] == "arm64": + print("Building for macOS 11.0+, platform arm64.") + env.Append(ASFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) + env.Append(CCFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) + env.Append(LINKFLAGS=["-arch", "arm64", "-mmacosx-version-min=11.0"]) + else: + print("Building for macOS 10.12+, platform x86-64.") + env.Append(ASFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) + env.Append(CCFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) + env.Append(LINKFLAGS=["-arch", "x86_64", "-mmacosx-version-min=10.12"]) + + cc_version = get_compiler_version(env) or [-1, -1] + vanilla = is_vanilla_clang(env) + + # Workaround for Xcode 15 linker bug. + if not vanilla and cc_version[0] == 15 and cc_version[1] == 0: + env.Prepend(LINKFLAGS=["-ld_classic"]) + + if not "osxcross" in env: # regular native build + if env["macports_clang"] != "no": + mpprefix = os.environ.get("MACPORTS_PREFIX", "/opt/local") + mpclangver = env["macports_clang"] + env["CC"] = mpprefix + "/libexec/llvm-" + mpclangver + "/bin/clang" + env["CXX"] = mpprefix + "/libexec/llvm-" + mpclangver + "/bin/clang++" + env["AR"] = mpprefix + "/libexec/llvm-" + mpclangver + "/bin/llvm-ar" + env["RANLIB"] = mpprefix + "/libexec/llvm-" + mpclangver + "/bin/llvm-ranlib" + env["AS"] = mpprefix + "/libexec/llvm-" + mpclangver + "/bin/llvm-as" + env.Append(CPPDEFINES=["__MACPORTS__"]) # hack to fix libvpx MM256_BROADCASTSI128_SI256 define + else: + env["CC"] = "clang" + env["CXX"] = "clang++" + + detect_darwin_sdk_path("osx", env) + env.Append(CCFLAGS=["-isysroot", "$MACOS_SDK_PATH"]) + env.Append(LINKFLAGS=["-isysroot", "$MACOS_SDK_PATH"]) + + else: # osxcross build + root = os.environ.get("OSXCROSS_ROOT", 0) + if env["arch"] == "arm64": + basecmd = root + "/target/bin/arm64-apple-" + env["osxcross_sdk"] + "-" + else: + basecmd = root + "/target/bin/x86_64-apple-" + env["osxcross_sdk"] + "-" + + ccache_path = os.environ.get("CCACHE") + if ccache_path is None: + env["CC"] = basecmd + "cc" + env["CXX"] = basecmd + "c++" + else: + # there aren't any ccache wrappers available for OS X cross-compile, + # to enable caching we need to prepend the path to the ccache binary + env["CC"] = ccache_path + " " + basecmd + "cc" + env["CXX"] = ccache_path + " " + basecmd + "c++" + env["AR"] = basecmd + "ar" + env["RANLIB"] = basecmd + "ranlib" + env["AS"] = basecmd + "as" + env.Append(CPPDEFINES=["__MACPORTS__"]) # hack to fix libvpx MM256_BROADCASTSI128_SI256 define + + # LTO + + if env["lto"] == "auto": # LTO benefits for macOS (size, performance) haven't been clearly established yet. + env["lto"] = "none" + + if env["lto"] != "none": + if env["lto"] == "thin": + env.Append(CCFLAGS=["-flto=thin"]) + env.Append(LINKFLAGS=["-flto=thin"]) + else: + env.Append(CCFLAGS=["-flto"]) + env.Append(LINKFLAGS=["-flto"]) + + # Sanitizers + + if env["use_ubsan"] or env["use_asan"] or env["use_lsan"] or env["use_tsan"]: + env.extra_suffix += "s" + + if env["use_ubsan"]: + env.Append(CCFLAGS=["-fsanitize=undefined"]) + env.Append(LINKFLAGS=["-fsanitize=undefined"]) + + if env["use_asan"]: + env.Append(CCFLAGS=["-fsanitize=address"]) + env.Append(LINKFLAGS=["-fsanitize=address"]) + + if env["use_lsan"]: + env.Append(CCFLAGS=["-fsanitize=leak"]) + env.Append(LINKFLAGS=["-fsanitize=leak"]) + + if env["use_tsan"]: + env.Append(CCFLAGS=["-fsanitize=thread"]) + env.Append(LINKFLAGS=["-fsanitize=thread"]) + + ## Dependencies + + if env["builtin_libtheora"]: + if env["arch"] != "arm64": + env["x86_libtheora_opt_gcc"] = True + + ## Flags + + env.Prepend(CPPPATH=["#platform/osx"]) + env.Append( + CPPDEFINES=[ + "OSX_ENABLED", + "UNIX_ENABLED", + "GLES_ENABLED", + "APPLE_STYLE_KEYS", + "COREAUDIO_ENABLED", + "COREMIDI_ENABLED", + "GL_SILENCE_DEPRECATION", + ] + ) + env.Append( + LINKFLAGS=[ + "-framework", + "Cocoa", + "-framework", + "Carbon", + "-framework", + "OpenGL", + "-framework", + "AGL", + "-framework", + "AudioUnit", + "-framework", + "CoreAudio", + "-framework", + "CoreMIDI", + "-lz", + "-framework", + "IOKit", + "-framework", + "ForceFeedback", + "-framework", + "AVFoundation", + "-framework", + "CoreMedia", + "-framework", + "CoreVideo", + ] + ) + env.Append(LIBS=["pthread"]) diff --git a/platform/windows/detect.py b/platform/windows/detect.py new file mode 100644 index 0000000..f27af66 --- /dev/null +++ b/platform/windows/detect.py @@ -0,0 +1,492 @@ +import methods +import os + +# To match other platforms +STACK_SIZE = 8388608 + + +def is_active(): + return True + + +def get_name(): + return "Windows" + + +def can_build(): + if os.name == "nt": + # Building natively on Windows + # If VCINSTALLDIR is set in the OS environ, use traditional Pandemonium logic to set up MSVC + if os.getenv("VCINSTALLDIR"): # MSVC, manual setup + return True + + # Otherwise, let SCons find MSVC if installed, or else Mingw. + # Since we're just returning True here, if there's no compiler + # installed, we'll get errors when it tries to build with the + # null compiler. + return True + + if os.name == "posix": + # Cross-compiling with MinGW-w64 (old MinGW32 is not supported) + mingw32 = "i686-w64-mingw32-" + mingw64 = "x86_64-w64-mingw32-" + + if os.getenv("MINGW32_PREFIX"): + mingw32 = os.getenv("MINGW32_PREFIX") + if os.getenv("MINGW64_PREFIX"): + mingw64 = os.getenv("MINGW64_PREFIX") + + test = "gcc --version > /dev/null 2>&1" + if os.system(mingw64 + test) == 0 or os.system(mingw32 + test) == 0: + return True + + return False + + +def get_opts(): + from SCons.Variables import BoolVariable, EnumVariable + + mingw32 = "" + mingw64 = "" + if os.name == "posix": + mingw32 = "i686-w64-mingw32-" + mingw64 = "x86_64-w64-mingw32-" + + if os.getenv("MINGW32_PREFIX"): + mingw32 = os.getenv("MINGW32_PREFIX") + if os.getenv("MINGW64_PREFIX"): + mingw64 = os.getenv("MINGW64_PREFIX") + + return [ + ("mingw_prefix_32", "MinGW prefix (Win32)", mingw32), + ("mingw_prefix_64", "MinGW prefix (Win64)", mingw64), + # Targeted Windows version: 7 (and later), minimum supported version + # XP support dropped after EOL due to missing API for IPv6 and other issues + # Vista support dropped after EOL due to GH-10243 + ("target_win_version", "Targeted Windows version, >= 0x0601 (Windows 7)", "0x0601"), + BoolVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", True), + EnumVariable("windows_subsystem", "Windows subsystem", "gui", ("gui", "console")), + BoolVariable("separate_debug_symbols", "Create a separate file containing debugging symbols", False), + ("msvc_version", "MSVC version to use. Ignored if VCINSTALLDIR is set in shell env.", None), + BoolVariable("use_mingw", "Use the Mingw compiler, even if MSVC is installed.", False), + BoolVariable("use_llvm", "Use the LLVM compiler", False), + BoolVariable("use_static_cpp", "Link MinGW/MSVC C++ runtime libraries statically", True), + BoolVariable("use_asan", "Use address sanitizer (ASAN)", False), + BoolVariable("incremental_link", "Use MSVC incremental linking. May increase or decrease build times.", False), + ] + + +def get_flags(): + return [] + + +def build_res_file(target, source, env): + if env["bits"] == "32": + cmdbase = env["mingw_prefix_32"] + else: + cmdbase = env["mingw_prefix_64"] + cmdbase = cmdbase + "windres --include-dir . " + import subprocess + + for x in range(len(source)): + cmd = cmdbase + "-i " + str(source[x]) + " -o " + str(target[x]) + try: + out = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE).communicate() + if len(out[1]): + return 1 + except Exception: + return 1 + return 0 + + +def setup_msvc_manual(env): + """Set up env to use MSVC manually, using VCINSTALLDIR""" + if env["bits"] != "default": + print( + """ + Bits argument is not supported for MSVC compilation. Architecture depends on the Native/Cross Compile Tools Prompt/Developer Console + (or Visual Studio settings) that is being used to run SCons. As a consequence, bits argument is disabled. Run scons again without bits + argument (example: scons p=windows) and SCons will attempt to detect what MSVC compiler will be executed and inform you. + """ + ) + raise SCons.Errors.UserError("Bits argument should not be used when using VCINSTALLDIR") + + # Force bits arg + # (Actually msys2 mingw can support 64-bit, we could detect that) + env["bits"] = "32" + env["x86_libtheora_opt_vc"] = True + + # find compiler manually + compiler_version_str = methods.detect_visual_c_compiler_version(env["ENV"]) + print("Found MSVC compiler: " + compiler_version_str) + + # If building for 64bit architecture, disable assembly optimisations for 32 bit builds (theora as of writing)... vc compiler for 64bit can not compile _asm + if compiler_version_str == "amd64" or compiler_version_str == "x86_amd64": + env["bits"] = "64" + env["x86_libtheora_opt_vc"] = False + print("Compiled program architecture will be a 64 bit executable (forcing bits=64).") + elif compiler_version_str == "x86" or compiler_version_str == "amd64_x86": + print("Compiled program architecture will be a 32 bit executable. (forcing bits=32).") + else: + print( + "Failed to manually detect MSVC compiler architecture version... Defaulting to 32bit executable settings (forcing bits=32). Compilation attempt will continue, but SCons can not detect for what architecture this build is compiled for. You should check your settings/compilation setup, or avoid setting VCINSTALLDIR." + ) + + +def setup_msvc_auto(env): + """Set up MSVC using SCons's auto-detection logic""" + + # If MSVC_VERSION is set by SCons, we know MSVC is installed. + # But we may want a different version or target arch. + + # The env may have already been set up with default MSVC tools, so + # reset a few things so we can set it up with the tools we want. + # (Ideally we'd decide on the tool config before configuring any + # environment, and just set the env up once, but this function runs + # on an existing env so this is the simplest way.) + env["MSVC_SETUP_RUN"] = False # Need to set this to re-run the tool + env["MSVS_VERSION"] = None + env["MSVC_VERSION"] = None + env["TARGET_ARCH"] = None + if env["bits"] != "default": + env["TARGET_ARCH"] = {"32": "x86", "64": "x86_64"}[env["bits"]] + if "msvc_version" in env: + env["MSVC_VERSION"] = env["msvc_version"] + env.Tool("msvc") + env.Tool("mssdk") # we want the MS SDK + # Note: actual compiler version can be found in env['MSVC_VERSION'], e.g. "14.1" for VS2015 + # Get actual target arch into bits (it may be "default" at this point): + if env["TARGET_ARCH"] in ("amd64", "x86_64"): + env["bits"] = "64" + else: + env["bits"] = "32" + print("Found MSVC version %s, arch %s, bits=%s" % (env["MSVC_VERSION"], env["TARGET_ARCH"], env["bits"])) + if env["TARGET_ARCH"] in ("amd64", "x86_64"): + env["x86_libtheora_opt_vc"] = False + + +def setup_mingw(env): + """Set up env for use with mingw""" + # Nothing to do here + print("Using MinGW") + + +def configure_msvc(env, manual_msvc_config): + """Configure env to work with MSVC""" + + # Build type + + if env["target"] == "release": + if env["optimize"] == "speed": # optimize for speed (default) + env.Append(CCFLAGS=["/O2"]) + env.Append(LINKFLAGS=["/OPT:REF"]) + elif env["optimize"] == "size": # optimize for size + env.Append(CCFLAGS=["/O1"]) + env.Append(LINKFLAGS=["/OPT:REF"]) + + elif env["target"] == "release_debug": + if env["optimize"] == "speed": # optimize for speed (default) + env.Append(CCFLAGS=["/O2"]) + env.Append(LINKFLAGS=["/OPT:REF"]) + elif env["optimize"] == "size": # optimize for size + env.Append(CCFLAGS=["/O1"]) + env.Append(LINKFLAGS=["/OPT:REF"]) + + elif env["target"] == "debug": + env.AppendUnique(CCFLAGS=["/Zi", "/FS", "/Od"]) + env.Append(LINKFLAGS=["/DEBUG"]) + + if env["windows_subsystem"] == "gui": + env.Append(LINKFLAGS=["/SUBSYSTEM:WINDOWS"]) + else: + env.Append(LINKFLAGS=["/SUBSYSTEM:CONSOLE"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) + + env.Append(LINKFLAGS=["/ENTRY:mainCRTStartup"]) + + if env["debug_symbols"]: + env.AppendUnique(CCFLAGS=["/Zi", "/FS"]) + env.AppendUnique(LINKFLAGS=["/DEBUG"]) + + ## Compile/link flags + + if env["use_static_cpp"]: + env.AppendUnique(CCFLAGS=["/MT"]) + else: + env.AppendUnique(CCFLAGS=["/MD"]) + + # MSVC incremental linking is broken and may _increase_ link time (GH-77968). + if not env["incremental_link"]: + env.Append(LINKFLAGS=["/INCREMENTAL:NO"]) + + env.AppendUnique(CCFLAGS=["/Gd", "/nologo"]) + env.AppendUnique(CCFLAGS=["/utf-8"]) # Force to use Unicode encoding. + env.AppendUnique(CXXFLAGS=["/TP"]) # assume all sources are C++ + # Once it was thought that only debug builds would be too large, + # but this has recently stopped being true. See the mingw function + # for notes on why this shouldn't be enabled for gcc + env.AppendUnique(CCFLAGS=["/bigobj"]) + + if manual_msvc_config: # should be automatic if SCons found it + if os.getenv("WindowsSdkDir") is not None: + env.Prepend(CPPPATH=[os.getenv("WindowsSdkDir") + "/Include"]) + else: + print("Missing environment variable: WindowsSdkDir") + + env.AppendUnique( + CPPDEFINES=[ + "WINDOWS_ENABLED", + "OPENGL_ENABLED", + "WASAPI_ENABLED", + "WINMIDI_ENABLED", + "TYPED_METHOD_BIND", + "WIN32", + "MSVC", + "WINVER=%s" % env["target_win_version"], + "_WIN32_WINNT=%s" % env["target_win_version"], + ] + ) + env.AppendUnique(CPPDEFINES=["NOMINMAX"]) # disable bogus min/max WinDef.h macros + if env["bits"] == "64": + env.AppendUnique(CPPDEFINES=["_WIN64"]) + + ## Libs + + LIBS = [ + "winmm", + "opengl32", + "dsound", + "kernel32", + "ole32", + "oleaut32", + "user32", + "gdi32", + "IPHLPAPI", + "Shlwapi", + "wsock32", + "Ws2_32", + "shell32", + "advapi32", + "dinput8", + "dxguid", + "imm32", + "bcrypt", + "Avrt", + "dwmapi", + ] + env.Append(LINKFLAGS=[p + env["LIBSUFFIX"] for p in LIBS]) + + if manual_msvc_config: + if os.getenv("WindowsSdkDir") is not None: + env.Append(LIBPATH=[os.getenv("WindowsSdkDir") + "/Lib"]) + else: + print("Missing environment variable: WindowsSdkDir") + + ## LTO + + if env["lto"] == "auto": # No LTO by default for MSVC, doesn't help. + env["lto"] = "none" + + if env["lto"] != "none": + if env["lto"] == "thin": + print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") + sys.exit(255) + env.AppendUnique(CCFLAGS=["/GL"]) + env.AppendUnique(ARFLAGS=["/LTCG"]) + if env["progress"]: + env.AppendUnique(LINKFLAGS=["/LTCG:STATUS"]) + else: + env.AppendUnique(LINKFLAGS=["/LTCG"]) + + if manual_msvc_config: + env.Prepend(CPPPATH=[p for p in os.getenv("INCLUDE").split(";")]) + env.Append(LIBPATH=[p for p in os.getenv("LIB").split(";")]) + + # Sanitizers + if env["use_asan"]: + env.extra_suffix += ".s" + env.Append(LINKFLAGS=["/INFERASANLIBS"]) + env.Append(CCFLAGS=["/fsanitize=address"]) + + # Incremental linking fix + env["BUILDERS"]["ProgramOriginal"] = env["BUILDERS"]["Program"] + env["BUILDERS"]["Program"] = methods.precious_program + + env.AppendUnique(LINKFLAGS=["/STACK:" + str(STACK_SIZE)]) + + +def configure_mingw(env): + # Workaround for MinGW. See: + # http://www.scons.org/wiki/LongCmdLinesOnWin32 + env.use_windows_spawn_fix() + + ## Build type + + if env["target"] == "release": + env.Append(CCFLAGS=["-msse2"]) + + if env["optimize"] == "speed": # optimize for speed (default) + if env["bits"] == "64": + env.Append(CCFLAGS=["-O3"]) + else: + env.Append(CCFLAGS=["-O2"]) + else: # optimize for size + env.Prepend(CCFLAGS=["-Os"]) + if env["debug_symbols"]: + env.Prepend(CCFLAGS=["-g2"]) + + elif env["target"] == "release_debug": + env.Append(CCFLAGS=["-O2"]) + if env["debug_symbols"]: + env.Prepend(CCFLAGS=["-g2"]) + if env["optimize"] == "speed": # optimize for speed (default) + env.Append(CCFLAGS=["-O2"]) + else: # optimize for size + env.Prepend(CCFLAGS=["-Os"]) + + elif env["target"] == "debug": + env.Append(CCFLAGS=["-g3"]) + # Allow big objects. It's supposed not to have drawbacks but seems to break + # GCC LTO, so enabling for debug builds only (which are not built with LTO + # and are the only ones with too big objects). + env.Append(CCFLAGS=["-Wa,-mbig-obj"]) + + if env["windows_subsystem"] == "gui": + env.Append(LINKFLAGS=["-Wl,--subsystem,windows"]) + else: + env.Append(LINKFLAGS=["-Wl,--subsystem,console"]) + env.AppendUnique(CPPDEFINES=["WINDOWS_SUBSYSTEM_CONSOLE"]) + + ## Compiler configuration + + if os.name == "nt": + # Force splitting libmodules.a in multiple chunks to work around + # issues reaching the linker command line size limit, which also + # seem to induce huge slowdown for 'ar' (GH-30892). + env["split_libmodules"] = True + else: + env["PROGSUFFIX"] = env["PROGSUFFIX"] + ".exe" # for linux cross-compilation + + if env["bits"] == "default": + if os.name == "nt": + env["bits"] = "64" if "PROGRAMFILES(X86)" in os.environ else "32" + else: # default to 64-bit on Linux + env["bits"] = "64" + + mingw_prefix = "" + + if env["bits"] == "32": + if env["use_static_cpp"]: + env.Append(LINKFLAGS=["-static"]) + env.Append(LINKFLAGS=["-static-libgcc"]) + env.Append(LINKFLAGS=["-static-libstdc++"]) + mingw_prefix = env["mingw_prefix_32"] + else: + if env["use_static_cpp"]: + env.Append(LINKFLAGS=["-static"]) + mingw_prefix = env["mingw_prefix_64"] + + if env["use_llvm"]: + env["CC"] = mingw_prefix + "clang" + env["CXX"] = mingw_prefix + "clang++" + env["AS"] = mingw_prefix + "as" + env["AR"] = mingw_prefix + "ar" + env["RANLIB"] = mingw_prefix + "ranlib" + else: + env["CC"] = mingw_prefix + "gcc" + env["CXX"] = mingw_prefix + "g++" + env["AS"] = mingw_prefix + "as" + env["AR"] = mingw_prefix + "gcc-ar" + env["RANLIB"] = mingw_prefix + "gcc-ranlib" + + env["x86_libtheora_opt_gcc"] = True + + ## LTO + + if env["lto"] == "auto": # Full LTO for production with MinGW. + env["lto"] = "full" + + if env["lto"] != "none": + if env["lto"] == "thin": + if not env["use_llvm"]: + print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") + sys.exit(255) + env.Append(CCFLAGS=["-flto=thin"]) + env.Append(LINKFLAGS=["-flto=thin"]) + elif not env["use_llvm"] and env.GetOption("num_jobs") > 1: + env.Append(CCFLAGS=["-flto"]) + env.Append(LINKFLAGS=["-flto=" + str(env.GetOption("num_jobs"))]) + else: + env.Append(CCFLAGS=["-flto"]) + env.Append(LINKFLAGS=["-flto"]) + + env.Append(LINKFLAGS=["-Wl,--stack," + str(STACK_SIZE)]) + + ## Compile flags + + env.Append(CCFLAGS=["-mwindows"]) + env.Append(LINKFLAGS=["-Wl,--nxcompat"]) # DEP protection. Not enabling ASLR for now, Mono crashes. + env.Append(CPPDEFINES=["WINDOWS_ENABLED", "OPENGL_ENABLED", "WASAPI_ENABLED", "WINMIDI_ENABLED"]) + env.Append(CPPDEFINES=[("WINVER", env["target_win_version"]), ("_WIN32_WINNT", env["target_win_version"])]) + env.Append( + LIBS=[ + "mingw32", + "opengl32", + "dsound", + "ole32", + "d3d9", + "winmm", + "gdi32", + "iphlpapi", + "shlwapi", + "wsock32", + "ws2_32", + "kernel32", + "oleaut32", + "dinput8", + "dxguid", + "ksuser", + "imm32", + "bcrypt", + "avrt", + "uuid", + "dwmapi", + ] + ) + + env.Append(CPPDEFINES=["MINGW_ENABLED", ("MINGW_HAS_SECURE_API", 1)]) + + # resrc + env.Append(BUILDERS={"RES": env.Builder(action=build_res_file, suffix=".o", src_suffix=".rc")}) + + +def configure(env): + # At this point the env has been set up with basic tools/compilers. + env.Prepend(CPPPATH=["#platform/windows"]) + + print("Configuring for Windows: target=%s, bits=%s" % (env["target"], env["bits"])) + + if os.name == "nt": + env["ENV"] = os.environ # this makes build less repeatable, but simplifies some things + env["ENV"]["TMP"] = os.environ["TMP"] + + # First figure out which compiler, version, and target arch we're using + if os.getenv("VCINSTALLDIR") and not env["use_mingw"]: + # Manual setup of MSVC + setup_msvc_manual(env) + env.msvc = True + manual_msvc_config = True + elif env.get("MSVC_VERSION", "") and not env["use_mingw"]: + setup_msvc_auto(env) + env.msvc = True + manual_msvc_config = False + else: + setup_mingw(env) + env.msvc = False + + # Now set compiler/linker flags + if env.msvc: + configure_msvc(env, manual_msvc_config) + + else: # MinGW + configure_mingw(env) diff --git a/platform/x11/detect.py b/platform/x11/detect.py new file mode 100644 index 0000000..ee2ea5b --- /dev/null +++ b/platform/x11/detect.py @@ -0,0 +1,423 @@ +import os +import platform +import sys +from methods import get_compiler_version, using_gcc, using_clang + + +def is_active(): + return True + + +def get_name(): + return "X11" + + +def can_build(): + if os.name != "posix" or sys.platform == "darwin": + return False + + # Check the minimal dependencies + x11_error = os.system("pkg-config --version > /dev/null") + if x11_error: + print("Error: pkg-config not found. Aborting.") + return False + + x11_error = os.system("pkg-config x11 --modversion > /dev/null") + if x11_error: + print("Error: X11 libraries not found. Aborting.") + return False + + x11_error = os.system("pkg-config xcursor --modversion > /dev/null") + if x11_error: + print("Error: Xcursor library not found. Aborting.") + return False + + x11_error = os.system("pkg-config xinerama --modversion > /dev/null") + if x11_error: + print("Error: Xinerama library not found. Aborting.") + return False + + x11_error = os.system("pkg-config xext --modversion > /dev/null") + if x11_error: + print("Error: Xext library not found. Aborting.") + return False + + x11_error = os.system("pkg-config xrandr --modversion > /dev/null") + if x11_error: + print("Error: XrandR library not found. Aborting.") + return False + + x11_error = os.system("pkg-config xrender --modversion > /dev/null") + if x11_error: + print("Error: XRender library not found. Aborting.") + return False + + x11_error = os.system("pkg-config xi --modversion > /dev/null") + if x11_error: + print("Error: Xi library not found. Aborting.") + return False + + return True + + +def get_opts(): + from SCons.Variables import BoolVariable, EnumVariable + + return [ + EnumVariable("linker", "Linker program", "default", ("default", "bfd", "gold", "lld", "mold")), + BoolVariable("use_llvm", "Use the LLVM compiler", False), + BoolVariable("use_static_cpp", "Link libgcc and libstdc++ statically for better portability", True), + BoolVariable('use_coverage', 'Test Pandemonium coverage', False), + BoolVariable("use_ubsan", "Use LLVM/GCC compiler undefined behavior sanitizer (UBSAN)", False), + BoolVariable("use_asan", "Use LLVM/GCC compiler address sanitizer (ASAN))", False), + BoolVariable("use_lsan", "Use LLVM/GCC compiler leak sanitizer (LSAN))", False), + BoolVariable("use_tsan", "Use LLVM/GCC compiler thread sanitizer (TSAN))", False), + BoolVariable("use_msan", "Use LLVM/GCC compiler memory sanitizer (MSAN))", False), + BoolVariable("pulseaudio", "Detect and use PulseAudio", True), + BoolVariable("udev", "Use udev for gamepad connection callbacks", True), + BoolVariable("debug_symbols", "Add debugging symbols to release/release_debug builds", True), + BoolVariable("separate_debug_symbols", "Create a separate file containing debugging symbols", False), + BoolVariable("touch", "Enable touch events", True), + BoolVariable("execinfo", "Use libexecinfo on systems where glibc is not available", False), + ] + + +def get_flags(): + return [] + + +def configure(env): + ## Build type + + if env["target"] == "release": + if env["optimize"] == "speed": # optimize for speed (default) + env.Prepend(CCFLAGS=["-O3"]) + elif env["optimize"] == "size": # optimize for size + env.Prepend(CCFLAGS=["-Os"]) + + if env["debug_symbols"]: + env.Prepend(CCFLAGS=["-g2"]) + + elif env["target"] == "release_debug": + if env["optimize"] == "speed": # optimize for speed (default) + env.Prepend(CCFLAGS=["-O2"]) + elif env["optimize"] == "size": # optimize for size + env.Prepend(CCFLAGS=["-Os"]) + + if env["debug_symbols"]: + env.Prepend(CCFLAGS=["-g2"]) + + elif env["target"] == "debug": + env.Prepend(CCFLAGS=["-ggdb"]) + env.Prepend(CCFLAGS=["-g3"]) + env.Append(LINKFLAGS=["-rdynamic"]) + + if env["debug_symbols"]: + # Adding dwarf-4 explicitly makes stacktraces work with clang builds, + # otherwise addr2line doesn't understand them + env.Append(CCFLAGS=["-gdwarf-4"]) + + ## Architecture + + is64 = sys.maxsize > 2**32 + if env["bits"] == "default": + env["bits"] = "64" if is64 else "32" + + machines = { + "riscv64": "rv64", + "ppc64le": "ppc64", + "ppc64": "ppc64", + "ppcle": "ppc", + "ppc": "ppc", + } + + if env["arch"] == "" and platform.machine() in machines: + env["arch"] = machines[platform.machine()] + + if env["arch"] == "rv64": + # G = General-purpose extensions, C = Compression extension (very common). + env.Append(CCFLAGS=["-march=rv64gc"]) + + ## Compiler configuration + + if "CXX" in env and "clang" in os.path.basename(env["CXX"]): + # Convenience check to enforce the use_llvm overrides when CXX is clang(++) + env["use_llvm"] = True + + if env["use_llvm"]: + if "clang++" not in os.path.basename(env["CXX"]): + env["CC"] = "clang" + env["CXX"] = "clang++" + env.extra_suffix = ".llvm" + env.extra_suffix + + # Linker + + if env["linker"] != "default": + print("Using linker program: " + env["linker"]) + if env["linker"] == "mold" and using_gcc(env): # GCC < 12.1 doesn't support -fuse-ld=mold. + cc_semver = tuple(get_compiler_version(env)) + if cc_semver < (12, 1): + found_wrapper = False + for path in ["/usr/libexec", "/usr/local/libexec", "/usr/lib", "/usr/local/lib"]: + if os.path.isfile(path + "/mold/ld"): + env.Append(LINKFLAGS=["-B" + path + "/mold"]) + found_wrapper = True + break + if not found_wrapper: + print("Couldn't locate mold installation path. Make sure it's installed in /usr or /usr/local.") + sys.exit(255) + else: + env.Append(LINKFLAGS=["-fuse-ld=mold"]) + else: + env.Append(LINKFLAGS=["-fuse-ld=%s" % env["linker"]]) + + if env['use_coverage']: + env.Append(CCFLAGS=['-ftest-coverage', '-fprofile-arcs']) + env.Append(LINKFLAGS=['-ftest-coverage', '-fprofile-arcs']) + + # Sanitizers + if env["use_ubsan"] or env["use_asan"] or env["use_lsan"] or env["use_tsan"] or env["use_msan"]: + env.extra_suffix += "s" + + if env["use_ubsan"]: + env.Append( + CCFLAGS=[ + "-fsanitize=undefined,shift,shift-exponent,integer-divide-by-zero,unreachable,vla-bound,null,return,signed-integer-overflow,bounds,float-divide-by-zero,float-cast-overflow,nonnull-attribute,returns-nonnull-attribute,bool,enum,vptr,pointer-overflow,builtin" + ] + ) + + if env["use_llvm"]: + env.Append( + CCFLAGS=[ + "-fsanitize=nullability-return,nullability-arg,function,nullability-assign,implicit-integer-sign-change,implicit-signed-integer-truncation,implicit-unsigned-integer-truncation" + ] + ) + else: + env.Append(CCFLAGS=["-fsanitize=bounds-strict"]) + env.Append(LINKFLAGS=["-fsanitize=undefined"]) + + if env["use_asan"]: + env.Append(CCFLAGS=["-fsanitize=address,pointer-subtract,pointer-compare"]) + env.Append(LINKFLAGS=["-fsanitize=address"]) + + if env["use_lsan"]: + env.Append(CCFLAGS=["-fsanitize=leak"]) + env.Append(LINKFLAGS=["-fsanitize=leak"]) + + if env["use_tsan"]: + env.Append(CCFLAGS=["-fsanitize=thread"]) + env.Append(LINKFLAGS=["-fsanitize=thread"]) + + if env["use_msan"]: + env.Append(CCFLAGS=["-fsanitize=memory"]) + env.Append(LINKFLAGS=["-fsanitize=memory"]) + + # LTO + + if env["lto"] == "auto": # Full LTO for production. + env["lto"] = "full" + + if env["lto"] != "none": + if env["lto"] == "thin": + if not env["use_llvm"]: + print("ThinLTO is only compatible with LLVM, use `use_llvm=yes` or `lto=full`.") + sys.exit(255) + env.Append(CCFLAGS=["-flto=thin"]) + env.Append(LINKFLAGS=["-flto=thin"]) + elif not env["use_llvm"] and env.GetOption("num_jobs") > 1: + env.Append(CCFLAGS=["-flto"]) + env.Append(LINKFLAGS=["-flto=" + str(env.GetOption("num_jobs"))]) + else: + env.Append(CCFLAGS=["-flto"]) + env.Append(LINKFLAGS=["-flto"]) + + if not env["use_llvm"]: + env["RANLIB"] = "gcc-ranlib" + env["AR"] = "gcc-ar" + + env.Append(CCFLAGS=["-pipe"]) + + # Check for gcc version >= 6 before adding -no-pie + version = get_compiler_version(env) or [-1, -1] + if using_gcc(env): + if version[0] >= 6: + env.Append(CCFLAGS=["-fpie"]) + env.Append(LINKFLAGS=["-no-pie"]) + # Do the same for clang should be fine with Clang 4 and higher + if using_clang(env): + if version[0] >= 4: + env.Append(CCFLAGS=["-fpie"]) + env.Append(LINKFLAGS=["-no-pie"]) + + ## Dependencies + + env.ParseConfig("pkg-config x11 --cflags --libs") + env.ParseConfig("pkg-config xcursor --cflags --libs") + env.ParseConfig("pkg-config xinerama --cflags --libs") + env.ParseConfig("pkg-config xext --cflags --libs") + env.ParseConfig("pkg-config xrandr --cflags --libs") + env.ParseConfig("pkg-config xrender --cflags --libs") + env.ParseConfig("pkg-config xi --cflags --libs") + + if env["touch"]: + env.Append(CPPDEFINES=["TOUCH_ENABLED"]) + + # FIXME: Check for existence of the libs before parsing their flags with pkg-config + + # freetype depends on libpng and zlib, so bundling one of them while keeping others + # as shared libraries leads to weird issues + if env["builtin_freetype"] or env["builtin_libpng"] or env["builtin_zlib"]: + env["builtin_freetype"] = True + env["builtin_libpng"] = True + env["builtin_zlib"] = True + + if not env["builtin_freetype"]: + env.ParseConfig("pkg-config freetype2 --cflags --libs") + + if not env["builtin_libpng"]: + env.ParseConfig("pkg-config libpng16 --cflags --libs") + + if False: # not env['builtin_assimp']: + # FIXME: Add min version check + env.ParseConfig("pkg-config assimp --cflags --libs") + + if not env["builtin_enet"]: + env.ParseConfig("pkg-config libenet --cflags --libs") + + if not env["builtin_squish"]: + env.ParseConfig("pkg-config libsquish --cflags --libs") + + if not env["builtin_zstd"]: + env.ParseConfig("pkg-config libzstd --cflags --libs") + + # Sound and video libraries + # Keep the order as it triggers chained dependencies (ogg needed by others, etc.) + + if not env["builtin_libtheora"]: + env["builtin_libogg"] = False # Needed to link against system libtheora + env["builtin_libvorbis"] = False # Needed to link against system libtheora + env.ParseConfig("pkg-config theora theoradec --cflags --libs") + else: + list_of_x86 = ["x86_64", "x86", "i386", "i586"] + if env["arch"] == "": + if any(platform.machine() in s for s in list_of_x86): + env["x86_libtheora_opt_gcc"] = True + else: + if any(env["arch"] in s for s in list_of_x86): + env["x86_libtheora_opt_gcc"] = True + + if not env["builtin_libvorbis"]: + env["builtin_libogg"] = False # Needed to link against system libvorbis + env.ParseConfig("pkg-config vorbis vorbisfile --cflags --libs") + + if not env["builtin_opus"]: + env["builtin_libogg"] = False # Needed to link against system opus + env.ParseConfig("pkg-config opus opusfile --cflags --libs") + + if not env["builtin_libogg"]: + env.ParseConfig("pkg-config ogg --cflags --libs") + + if not env["builtin_mbedtls"]: + # mbedTLS does not provide a pkgconfig config yet. See https://github.com/ARMmbed/mbedtls/issues/228 + env.Append(LIBS=["mbedtls", "mbedcrypto", "mbedx509"]) + + if not env["builtin_wslay"]: + env.ParseConfig("pkg-config libwslay --cflags --libs") + + if not env["builtin_miniupnpc"]: + # No pkgconfig file so far, hardcode default paths. + env.Prepend(CPPPATH=["/usr/include/miniupnpc"]) + env.Append(LIBS=["miniupnpc"]) + + # On Linux wchar_t should be 32-bits + # 16-bit library shouldn't be required due to compiler optimisations + if not env["builtin_pcre2"]: + env.ParseConfig("pkg-config libpcre2-32 --cflags --libs") + + ## Flags + + if os.system("pkg-config --exists alsa") == 0: # 0 means found + env["alsa"] = True + env.Append(CPPDEFINES=["ALSA_ENABLED", "ALSAMIDI_ENABLED"]) + env.ParseConfig("pkg-config alsa --cflags") # Only cflags, we dlopen the library. + else: + print("Warning: ALSA libraries not found. Disabling the ALSA audio driver.") + + if env["pulseaudio"]: + if os.system("pkg-config --exists libpulse") == 0: # 0 means found + env.Append(CPPDEFINES=["PULSEAUDIO_ENABLED"]) + env.ParseConfig("pkg-config libpulse --cflags") # Only cflags, we dlopen the library. + else: + print("Warning: PulseAudio development libraries not found. Disabling the PulseAudio audio driver.") + + if platform.system() == "Linux": + env.Append(CPPDEFINES=["JOYDEV_ENABLED"]) + if env["udev"]: + if os.system("pkg-config --exists libudev") == 0: # 0 means found + env.Append(CPPDEFINES=["UDEV_ENABLED"]) + env.ParseConfig("pkg-config libudev --cflags") # Only cflags, we dlopen the library. + else: + print("Warning: libudev development libraries not found. Disabling controller hotplugging support.") + else: + env["udev"] = False # Linux specific + + # Linkflags below this line should typically stay the last ones + if not env["builtin_zlib"]: + env.ParseConfig("pkg-config zlib --cflags --libs") + + env.Prepend(CPPPATH=["#platform/x11"]) + env.Append(CPPDEFINES=["X11_ENABLED", "UNIX_ENABLED", "OPENGL_ENABLED", "GLES_ENABLED", ("_FILE_OFFSET_BITS", 64)]) + + env.ParseConfig("pkg-config gl --cflags --libs") + + env.Append(LIBS=["pthread"]) + + if platform.system() == "Linux": + env.Append(LIBS=["dl"]) + + if not env["execinfo"] and platform.libc_ver()[0] != "glibc": + # The default crash handler depends on glibc, so if the host uses + # a different libc (BSD libc, musl), fall back to libexecinfo. + print("Note: Using `execinfo=yes` for the crash handler as required on platforms where glibc is missing.") + env["execinfo"] = True + + if env["execinfo"]: + env.Append(LIBS=["execinfo"]) + + if True:#not env["tools"]: + import subprocess + import re + + linker_version_str = subprocess.check_output( + [env.subst(env["LINK"]), "-Wl,--version"] + env.subst(env["LINKFLAGS"]) + ).decode("utf-8") + gnu_ld_version = re.search("^GNU ld [^$]*(\d+\.\d+)$", linker_version_str, re.MULTILINE) + if not gnu_ld_version: + print( + "Warning: Creating export template binaries enabled for PCK embedding is currently only supported with GNU ld, not gold, LLD or mold." + ) + else: + if float(gnu_ld_version.group(1)) >= 2.30: + env.Append(LINKFLAGS=["-T", "platform/x11/pck_embed.ld"]) + else: + env.Append(LINKFLAGS=["-T", "platform/x11/pck_embed.legacy.ld"]) + + ## Cross-compilation + + if is64 and env["bits"] == "32": + env.Append(CCFLAGS=["-m32"]) + env.Append(LINKFLAGS=["-m32", "-L/usr/lib/i386-linux-gnu"]) + elif not is64 and env["bits"] == "64": + env.Append(CCFLAGS=["-m64"]) + env.Append(LINKFLAGS=["-m64", "-L/usr/lib/i686-linux-gnu"]) + + # Link those statically for portability + if env["use_static_cpp"]: + env.Append(LINKFLAGS=["-static-libgcc", "-static-libstdc++"]) + if env["use_llvm"] and platform.system() != "FreeBSD": + #env["LINKCOM"] = env["LINKCOM"] + " -l:libatomic.a" + env["LINKCOM"] = env["LINKCOM"] + " -latomic" + + else: + if env["use_llvm"] and platform.system() != "FreeBSD": + env.Append(LIBS=["atomic"]) diff --git a/platform_methods.py b/platform_methods.py new file mode 100644 index 0000000..e72927b --- /dev/null +++ b/platform_methods.py @@ -0,0 +1,81 @@ +import os +import sys +import json +import uuid +import functools +import subprocess + +# NOTE: The multiprocessing module is not compatible with SCons due to conflict on cPickle + +if sys.version_info[0] < 3: + JSON_SERIALIZABLE_TYPES = (bool, int, long, float, basestring) +else: + JSON_SERIALIZABLE_TYPES = (bool, int, float, str) + + +def run_in_subprocess(builder_function): + @functools.wraps(builder_function) + def wrapper(target, source, env): + + # Convert SCons Node instances to absolute paths + target = [node.srcnode().abspath for node in target] + source = [node.srcnode().abspath for node in source] + + # Short circuit on non-Windows platforms, no need to run in subprocess + if sys.platform not in ("win32", "cygwin"): + return builder_function(target, source, env) + + # Identify module + module_name = builder_function.__module__ + function_name = builder_function.__name__ + module_path = sys.modules[module_name].__file__ + if module_path.endswith(".pyc") or module_path.endswith(".pyo"): + module_path = module_path[:-1] + + # Subprocess environment + subprocess_env = os.environ.copy() + subprocess_env["PYTHONPATH"] = os.pathsep.join([os.getcwd()] + sys.path) + + # Keep only JSON serializable environment items + filtered_env = dict((key, value) for key, value in env.items() if isinstance(value, JSON_SERIALIZABLE_TYPES)) + + # Save parameters + args = (target, source, filtered_env) + data = dict(fn=function_name, args=args) + json_path = os.path.join(os.environ["TMP"], uuid.uuid4().hex + ".json") + with open(json_path, "wt") as json_file: + json.dump(data, json_file, indent=2) + json_file_size = os.stat(json_path).st_size + + print( + "Executing builder function in subprocess: " + "module_path=%r, parameter_file=%r, parameter_file_size=%r, target=%r, source=%r" + % (module_path, json_path, json_file_size, target, source) + ) + try: + exit_code = subprocess.call([sys.executable, module_path, json_path], env=subprocess_env) + finally: + try: + os.remove(json_path) + except (OSError, IOError) as e: + # Do not fail the entire build if it cannot delete a temporary file + print( + "WARNING: Could not delete temporary file: path=%r; [%s] %s" % (json_path, e.__class__.__name__, e) + ) + + # Must succeed + if exit_code: + raise RuntimeError( + "Failed to run builder function in subprocess: module_path=%r; data=%r" % (module_path, data) + ) + + return wrapper + + +def subprocess_main(namespace): + + with open(sys.argv[1]) as json_file: + data = json.load(json_file) + + fn = namespace[data["fn"]] + fn(*data["args"])