1import os
2import re
3import glob
4import subprocess
5from collections import OrderedDict
6from compat import iteritems, isbasestring, open_utf8, decode_utf8, qualname
7
8
9def add_source_files(self, sources, files, warn_duplicates=True):
10    # Convert string to list of absolute paths (including expanding wildcard)
11    if isbasestring(files):
12        # Keep SCons project-absolute path as they are (no wildcard support)
13        if files.startswith("#"):
14            if "*" in files:
15                print("ERROR: Wildcards can't be expanded in SCons project-absolute path: '{}'".format(files))
16                return
17            files = [files]
18        else:
19            dir_path = self.Dir(".").abspath
20            files = sorted(glob.glob(dir_path + "/" + files))
21
22    # Add each path as compiled Object following environment (self) configuration
23    for path in files:
24        obj = self.Object(path)
25        if obj in sources:
26            if warn_duplicates:
27                print('WARNING: Object "{}" already included in environment sources.'.format(obj))
28            else:
29                continue
30        sources.append(obj)
31
32
33def disable_warnings(self):
34    # 'self' is the environment
35    if self.msvc:
36        # We have to remove existing warning level defines before appending /w,
37        # otherwise we get: "warning D9025 : overriding '/W3' with '/w'"
38        warn_flags = ["/Wall", "/W4", "/W3", "/W2", "/W1", "/WX"]
39        self.Append(CCFLAGS=["/w"])
40        self.Append(CFLAGS=["/w"])
41        self.Append(CXXFLAGS=["/w"])
42        self["CCFLAGS"] = [x for x in self["CCFLAGS"] if not x in warn_flags]
43        self["CFLAGS"] = [x for x in self["CFLAGS"] if not x in warn_flags]
44        self["CXXFLAGS"] = [x for x in self["CXXFLAGS"] if not x in warn_flags]
45    else:
46        self.Append(CCFLAGS=["-w"])
47        self.Append(CFLAGS=["-w"])
48        self.Append(CXXFLAGS=["-w"])
49
50
51def add_module_version_string(self, s):
52    self.module_version_string += "." + s
53
54
55def update_version(module_version_string=""):
56
57    build_name = "DragonFly_Ports_build"
58    if os.getenv("BUILD_NAME") != None:
59        build_name = os.getenv("BUILD_NAME")
60        print("Using custom build name: " + build_name)
61
62    import version
63
64    # NOTE: It is safe to generate this file here, since this is still executed serially
65    f = open("core/version_generated.gen.h", "w")
66    f.write('#define VERSION_SHORT_NAME "' + str(version.short_name) + '"\n')
67    f.write('#define VERSION_NAME "' + str(version.name) + '"\n')
68    f.write("#define VERSION_MAJOR " + str(version.major) + "\n")
69    f.write("#define VERSION_MINOR " + str(version.minor) + "\n")
70    f.write("#define VERSION_PATCH " + str(version.patch) + "\n")
71    f.write('#define VERSION_STATUS "' + str(version.status) + '"\n')
72    f.write('#define VERSION_BUILD "' + str(build_name) + '"\n')
73    f.write('#define VERSION_MODULE_CONFIG "' + str(version.module_config) + module_version_string + '"\n')
74    f.write("#define VERSION_YEAR " + str(version.year) + "\n")
75    f.write('#define VERSION_WEBSITE "' + str(version.website) + '"\n')
76    f.close()
77
78    # NOTE: It is safe to generate this file here, since this is still executed serially
79    fhash = open("core/version_hash.gen.h", "w")
80    githash = ""
81    gitfolder = ".git"
82
83    if os.path.isfile(".git"):
84        module_folder = open(".git", "r").readline().strip()
85        if module_folder.startswith("gitdir: "):
86            gitfolder = module_folder[8:]
87
88    if os.path.isfile(os.path.join(gitfolder, "HEAD")):
89        head = open_utf8(os.path.join(gitfolder, "HEAD"), "r").readline().strip()
90        if head.startswith("ref: "):
91            head = os.path.join(gitfolder, head[5:])
92            if os.path.isfile(head):
93                githash = open(head, "r").readline().strip()
94        else:
95            githash = head
96
97    fhash.write('#define VERSION_HASH "' + githash + '"')
98    fhash.close()
99
100
101def parse_cg_file(fname, uniforms, sizes, conditionals):
102
103    fs = open(fname, "r")
104    line = fs.readline()
105
106    while line:
107
108        if re.match(r"^\s*uniform", line):
109
110            res = re.match(r"uniform ([\d\w]*) ([\d\w]*)")
111            type = res.groups(1)
112            name = res.groups(2)
113
114            uniforms.append(name)
115
116            if type.find("texobj") != -1:
117                sizes.append(1)
118            else:
119                t = re.match(r"float(\d)x(\d)", type)
120                if t:
121                    sizes.append(int(t.groups(1)) * int(t.groups(2)))
122                else:
123                    t = re.match(r"float(\d)", type)
124                    sizes.append(int(t.groups(1)))
125
126            if line.find("[branch]") != -1:
127                conditionals.append(name)
128
129        line = fs.readline()
130
131    fs.close()
132
133
134def detect_modules(at_path):
135    module_list = OrderedDict()  # name : path
136
137    modules_glob = os.path.join(at_path, "*")
138    files = glob.glob(modules_glob)
139    files.sort()  # so register_module_types does not change that often, and also plugins are registered in alphabetic order
140
141    for x in files:
142        if not is_module(x):
143            continue
144        name = os.path.basename(x)
145        path = x.replace("\\", "/")  # win32
146        module_list[name] = path
147
148    return module_list
149
150
151def is_module(path):
152    return os.path.isdir(path) and os.path.exists(os.path.join(path, "SCsub"))
153
154
155def write_modules(module_list):
156    includes_cpp = ""
157    register_cpp = ""
158    unregister_cpp = ""
159
160    for name, path in module_list.items():
161        try:
162            with open(os.path.join(path, "register_types.h")):
163                includes_cpp += '#include "' + path + '/register_types.h"\n'
164                register_cpp += "#ifdef MODULE_" + name.upper() + "_ENABLED\n"
165                register_cpp += "\tregister_" + name + "_types();\n"
166                register_cpp += "#endif\n"
167                unregister_cpp += "#ifdef MODULE_" + name.upper() + "_ENABLED\n"
168                unregister_cpp += "\tunregister_" + name + "_types();\n"
169                unregister_cpp += "#endif\n"
170        except IOError:
171            pass
172
173    modules_cpp = (
174        """
175// modules.cpp - THIS FILE IS GENERATED, DO NOT EDIT!!!!!!!
176#include "register_module_types.h"
177
178"""
179        + includes_cpp
180        + """
181
182void register_module_types() {
183"""
184        + register_cpp
185        + """
186}
187
188void unregister_module_types() {
189"""
190        + unregister_cpp
191        + """
192}
193"""
194    )
195
196    # NOTE: It is safe to generate this file here, since this is still executed serially
197    with open("modules/register_module_types.gen.cpp", "w") as f:
198        f.write(modules_cpp)
199
200
201def convert_custom_modules_path(path):
202    if not path:
203        return path
204    path = os.path.realpath(os.path.expanduser(os.path.expandvars(path)))
205    err_msg = "Build option 'custom_modules' must %s"
206    if not os.path.isdir(path):
207        raise ValueError(err_msg % "point to an existing directory.")
208    if path == os.path.realpath("modules"):
209        raise ValueError(err_msg % "be a directory other than built-in `modules` directory.")
210    if is_module(path):
211        raise ValueError(err_msg % "point to a directory with modules, not a single module.")
212    return path
213
214
215def disable_module(self):
216    self.disabled_modules.append(self.current_module)
217
218
219def use_windows_spawn_fix(self, platform=None):
220
221    if os.name != "nt":
222        return  # not needed, only for windows
223
224    # On Windows, due to the limited command line length, when creating a static library
225    # from a very high number of objects SCons will invoke "ar" once per object file;
226    # that makes object files with same names to be overwritten so the last wins and
227    # the library looses symbols defined by overwritten objects.
228    # By enabling quick append instead of the default mode (replacing), libraries will
229    # got built correctly regardless the invocation strategy.
230    # Furthermore, since SCons will rebuild the library from scratch when an object file
231    # changes, no multiple versions of the same object file will be present.
232    self.Replace(ARFLAGS="q")
233
234    def mySubProcess(cmdline, env):
235
236        startupinfo = subprocess.STARTUPINFO()
237        startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
238        proc = subprocess.Popen(
239            cmdline,
240            stdin=subprocess.PIPE,
241            stdout=subprocess.PIPE,
242            stderr=subprocess.PIPE,
243            startupinfo=startupinfo,
244            shell=False,
245            env=env,
246        )
247        _, err = proc.communicate()
248        rv = proc.wait()
249        if rv:
250            print("=====")
251            print(err)
252            print("=====")
253        return rv
254
255    def mySpawn(sh, escape, cmd, args, env):
256
257        newargs = " ".join(args[1:])
258        cmdline = cmd + " " + newargs
259
260        rv = 0
261        env = {str(key): str(value) for key, value in iteritems(env)}
262        if len(cmdline) > 32000 and cmd.endswith("ar"):
263            cmdline = cmd + " " + args[1] + " " + args[2] + " "
264            for i in range(3, len(args)):
265                rv = mySubProcess(cmdline + args[i], env)
266                if rv:
267                    break
268        else:
269            rv = mySubProcess(cmdline, env)
270
271        return rv
272
273    self["SPAWN"] = mySpawn
274
275
276def split_lib(self, libname, src_list=None, env_lib=None):
277    env = self
278
279    num = 0
280    cur_base = ""
281    max_src = 64
282    list = []
283    lib_list = []
284
285    if src_list is None:
286        src_list = getattr(env, libname + "_sources")
287
288    if type(env_lib) == type(None):
289        env_lib = env
290
291    for f in src_list:
292        fname = ""
293        if type(f) == type(""):
294            fname = env.File(f).path
295        else:
296            fname = env.File(f)[0].path
297        fname = fname.replace("\\", "/")
298        base = "/".join(fname.split("/")[:2])
299        if base != cur_base and len(list) > max_src:
300            if num > 0:
301                lib = env_lib.add_library(libname + str(num), list)
302                lib_list.append(lib)
303                list = []
304            num = num + 1
305        cur_base = base
306        list.append(f)
307
308    lib = env_lib.add_library(libname + str(num), list)
309    lib_list.append(lib)
310
311    lib_base = []
312    env_lib.add_source_files(lib_base, "*.cpp")
313    lib = env_lib.add_library(libname, lib_base)
314    lib_list.insert(0, lib)
315
316    env.Prepend(LIBS=lib_list)
317
318    # When we split modules into arbitrary chunks, we end up with linking issues
319    # due to symbol dependencies split over several libs, which may not be linked
320    # in the required order. We use --start-group and --end-group to tell the
321    # linker that those archives should be searched repeatedly to resolve all
322    # undefined references.
323    # As SCons doesn't give us much control over how inserting libs in LIBS
324    # impacts the linker call, we need to hack our way into the linking commands
325    # LINKCOM and SHLINKCOM to set those flags.
326
327    if "-Wl,--start-group" in env["LINKCOM"] and "-Wl,--start-group" in env["SHLINKCOM"]:
328        # Already added by a previous call, skip.
329        return
330
331    env["LINKCOM"] = str(env["LINKCOM"]).replace("$_LIBFLAGS", "-Wl,--start-group $_LIBFLAGS -Wl,--end-group")
332    env["SHLINKCOM"] = str(env["LINKCOM"]).replace("$_LIBFLAGS", "-Wl,--start-group $_LIBFLAGS -Wl,--end-group")
333
334
335def save_active_platforms(apnames, ap):
336
337    for x in ap:
338        names = ["logo"]
339        if os.path.isfile(x + "/run_icon.png"):
340            names.append("run_icon")
341
342        for name in names:
343            pngf = open(x + "/" + name + ".png", "rb")
344            b = pngf.read(1)
345            str = " /* AUTOGENERATED FILE, DO NOT EDIT */ \n"
346            str += " static const unsigned char _" + x[9:] + "_" + name + "[]={"
347            while len(b) == 1:
348                str += hex(ord(b))
349                b = pngf.read(1)
350                if len(b) == 1:
351                    str += ","
352
353            str += "};\n"
354
355            pngf.close()
356
357            # NOTE: It is safe to generate this file here, since this is still executed serially
358            wf = x + "/" + name + ".gen.h"
359            with open(wf, "w") as pngw:
360                pngw.write(str)
361
362
363def no_verbose(sys, env):
364
365    colors = {}
366
367    # Colors are disabled in non-TTY environments such as pipes. This means
368    # that if output is redirected to a file, it will not contain color codes
369    if sys.stdout.isatty():
370        colors["cyan"] = "\033[96m"
371        colors["purple"] = "\033[95m"
372        colors["blue"] = "\033[94m"
373        colors["green"] = "\033[92m"
374        colors["yellow"] = "\033[93m"
375        colors["red"] = "\033[91m"
376        colors["end"] = "\033[0m"
377    else:
378        colors["cyan"] = ""
379        colors["purple"] = ""
380        colors["blue"] = ""
381        colors["green"] = ""
382        colors["yellow"] = ""
383        colors["red"] = ""
384        colors["end"] = ""
385
386    compile_source_message = "%sCompiling %s==> %s$SOURCE%s" % (
387        colors["blue"],
388        colors["purple"],
389        colors["yellow"],
390        colors["end"],
391    )
392    java_compile_source_message = "%sCompiling %s==> %s$SOURCE%s" % (
393        colors["blue"],
394        colors["purple"],
395        colors["yellow"],
396        colors["end"],
397    )
398    compile_shared_source_message = "%sCompiling shared %s==> %s$SOURCE%s" % (
399        colors["blue"],
400        colors["purple"],
401        colors["yellow"],
402        colors["end"],
403    )
404    link_program_message = "%sLinking Program        %s==> %s$TARGET%s" % (
405        colors["red"],
406        colors["purple"],
407        colors["yellow"],
408        colors["end"],
409    )
410    link_library_message = "%sLinking Static Library %s==> %s$TARGET%s" % (
411        colors["red"],
412        colors["purple"],
413        colors["yellow"],
414        colors["end"],
415    )
416    ranlib_library_message = "%sRanlib Library         %s==> %s$TARGET%s" % (
417        colors["red"],
418        colors["purple"],
419        colors["yellow"],
420        colors["end"],
421    )
422    link_shared_library_message = "%sLinking Shared Library %s==> %s$TARGET%s" % (
423        colors["red"],
424        colors["purple"],
425        colors["yellow"],
426        colors["end"],
427    )
428    java_library_message = "%sCreating Java Archive  %s==> %s$TARGET%s" % (
429        colors["red"],
430        colors["purple"],
431        colors["yellow"],
432        colors["end"],
433    )
434
435    env.Append(CXXCOMSTR=[compile_source_message])
436    env.Append(CCCOMSTR=[compile_source_message])
437    env.Append(SHCCCOMSTR=[compile_shared_source_message])
438    env.Append(SHCXXCOMSTR=[compile_shared_source_message])
439    env.Append(ARCOMSTR=[link_library_message])
440    env.Append(RANLIBCOMSTR=[ranlib_library_message])
441    env.Append(SHLINKCOMSTR=[link_shared_library_message])
442    env.Append(LINKCOMSTR=[link_program_message])
443    env.Append(JARCOMSTR=[java_library_message])
444    env.Append(JAVACCOMSTR=[java_compile_source_message])
445
446
447def detect_visual_c_compiler_version(tools_env):
448    # tools_env is the variable scons uses to call tools that execute tasks, SCons's env['ENV'] that executes tasks...
449    # (see the SCons documentation for more information on what it does)...
450    # in order for this function to be well encapsulated i choose to force it to receive SCons's TOOLS env (env['ENV']
451    # and not scons setup environment (env)... so make sure you call the right environment on it or it will fail to detect
452    # the proper vc version that will be called
453
454    # There is no flag to give to visual c compilers to set the architecture, ie scons bits argument (32,64,ARM etc)
455    # There are many different cl.exe files that are run, and each one compiles & links to a different architecture
456    # As far as I know, the only way to figure out what compiler will be run when Scons calls cl.exe via Program()
457    # is to check the PATH variable and figure out which one will be called first. Code below does that and returns:
458    # the following string values:
459
460    # ""              Compiler not detected
461    # "amd64"         Native 64 bit compiler
462    # "amd64_x86"     64 bit Cross Compiler for 32 bit
463    # "x86"           Native 32 bit compiler
464    # "x86_amd64"     32 bit Cross Compiler for 64 bit
465
466    # There are other architectures, but Godot does not support them currently, so this function does not detect arm/amd64_arm
467    # and similar architectures/compilers
468
469    # Set chosen compiler to "not detected"
470    vc_chosen_compiler_index = -1
471    vc_chosen_compiler_str = ""
472
473    # Start with Pre VS 2017 checks which uses VCINSTALLDIR:
474    if "VCINSTALLDIR" in tools_env:
475        # print("Checking VCINSTALLDIR")
476
477        # find() works with -1 so big ifs below are needed... the simplest solution, in fact
478        # First test if amd64 and amd64_x86 compilers are present in the path
479        vc_amd64_compiler_detection_index = tools_env["PATH"].find(tools_env["VCINSTALLDIR"] + "BIN\\amd64;")
480        if vc_amd64_compiler_detection_index > -1:
481            vc_chosen_compiler_index = vc_amd64_compiler_detection_index
482            vc_chosen_compiler_str = "amd64"
483
484        vc_amd64_x86_compiler_detection_index = tools_env["PATH"].find(tools_env["VCINSTALLDIR"] + "BIN\\amd64_x86;")
485        if vc_amd64_x86_compiler_detection_index > -1 and (
486            vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_amd64_x86_compiler_detection_index
487        ):
488            vc_chosen_compiler_index = vc_amd64_x86_compiler_detection_index
489            vc_chosen_compiler_str = "amd64_x86"
490
491        # Now check the 32 bit compilers
492        vc_x86_compiler_detection_index = tools_env["PATH"].find(tools_env["VCINSTALLDIR"] + "BIN;")
493        if vc_x86_compiler_detection_index > -1 and (
494            vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_x86_compiler_detection_index
495        ):
496            vc_chosen_compiler_index = vc_x86_compiler_detection_index
497            vc_chosen_compiler_str = "x86"
498
499        vc_x86_amd64_compiler_detection_index = tools_env["PATH"].find(tools_env["VCINSTALLDIR"] + "BIN\\x86_amd64;")
500        if vc_x86_amd64_compiler_detection_index > -1 and (
501            vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_x86_amd64_compiler_detection_index
502        ):
503            vc_chosen_compiler_index = vc_x86_amd64_compiler_detection_index
504            vc_chosen_compiler_str = "x86_amd64"
505
506    # and for VS 2017 and newer we check VCTOOLSINSTALLDIR:
507    if "VCTOOLSINSTALLDIR" in tools_env:
508
509        # Newer versions have a different path available
510        vc_amd64_compiler_detection_index = (
511            tools_env["PATH"].upper().find(tools_env["VCTOOLSINSTALLDIR"].upper() + "BIN\\HOSTX64\\X64;")
512        )
513        if vc_amd64_compiler_detection_index > -1:
514            vc_chosen_compiler_index = vc_amd64_compiler_detection_index
515            vc_chosen_compiler_str = "amd64"
516
517        vc_amd64_x86_compiler_detection_index = (
518            tools_env["PATH"].upper().find(tools_env["VCTOOLSINSTALLDIR"].upper() + "BIN\\HOSTX64\\X86;")
519        )
520        if vc_amd64_x86_compiler_detection_index > -1 and (
521            vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_amd64_x86_compiler_detection_index
522        ):
523            vc_chosen_compiler_index = vc_amd64_x86_compiler_detection_index
524            vc_chosen_compiler_str = "amd64_x86"
525
526        vc_x86_compiler_detection_index = (
527            tools_env["PATH"].upper().find(tools_env["VCTOOLSINSTALLDIR"].upper() + "BIN\\HOSTX86\\X86;")
528        )
529        if vc_x86_compiler_detection_index > -1 and (
530            vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_x86_compiler_detection_index
531        ):
532            vc_chosen_compiler_index = vc_x86_compiler_detection_index
533            vc_chosen_compiler_str = "x86"
534
535        vc_x86_amd64_compiler_detection_index = (
536            tools_env["PATH"].upper().find(tools_env["VCTOOLSINSTALLDIR"].upper() + "BIN\\HOSTX86\\X64;")
537        )
538        if vc_x86_amd64_compiler_detection_index > -1 and (
539            vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_x86_amd64_compiler_detection_index
540        ):
541            vc_chosen_compiler_index = vc_x86_amd64_compiler_detection_index
542            vc_chosen_compiler_str = "x86_amd64"
543
544    return vc_chosen_compiler_str
545
546
547def find_visual_c_batch_file(env):
548    from SCons.Tool.MSCommon.vc import get_default_version, get_host_target, find_batch_file
549
550    version = get_default_version(env)
551    (host_platform, target_platform, _) = get_host_target(env)
552    return find_batch_file(env, version, host_platform, target_platform)[0]
553
554
555def generate_cpp_hint_file(filename):
556    if os.path.isfile(filename):
557        # Don't overwrite an existing hint file since the user may have customized it.
558        pass
559    else:
560        try:
561            with open(filename, "w") as fd:
562                fd.write("#define GDCLASS(m_class, m_inherits)\n")
563        except IOError:
564            print("Could not write cpp.hint file.")
565
566
567def generate_vs_project(env, num_jobs):
568    batch_file = find_visual_c_batch_file(env)
569    if batch_file:
570
571        def build_commandline(commands):
572            common_build_prefix = [
573                'cmd /V /C set "plat=$(PlatformTarget)"',
574                '(if "$(PlatformTarget)"=="x64" (set "plat=x86_amd64"))',
575                'set "tools=yes"',
576                '(if "$(Configuration)"=="release" (set "tools=no"))',
577                'set "custom_modules=%s"' % env["custom_modules"],
578                'call "' + batch_file + '" !plat!',
579            ]
580
581            result = " ^& ".join(common_build_prefix + [commands])
582            return result
583
584        env.AddToVSProject(env.core_sources)
585        env.AddToVSProject(env.main_sources)
586        env.AddToVSProject(env.modules_sources)
587        env.AddToVSProject(env.scene_sources)
588        env.AddToVSProject(env.servers_sources)
589        env.AddToVSProject(env.editor_sources)
590
591        # windows allows us to have spaces in paths, so we need
592        # to double quote off the directory. However, the path ends
593        # in a backslash, so we need to remove this, lest it escape the
594        # last double quote off, confusing MSBuild
595        env["MSVSBUILDCOM"] = build_commandline(
596            "scons --directory=\"$(ProjectDir.TrimEnd('\\'))\" platform=windows progress=no target=$(Configuration) tools=!tools! custom_modules=!custom_modules! -j"
597            + str(num_jobs)
598        )
599        env["MSVSREBUILDCOM"] = build_commandline(
600            "scons --directory=\"$(ProjectDir.TrimEnd('\\'))\" platform=windows progress=no target=$(Configuration) tools=!tools! vsproj=yes custom_modules=!custom_modules! -j"
601            + str(num_jobs)
602        )
603        env["MSVSCLEANCOM"] = build_commandline(
604            "scons --directory=\"$(ProjectDir.TrimEnd('\\'))\" --clean platform=windows progress=no target=$(Configuration) tools=!tools! custom_modules=!custom_modules! -j"
605            + str(num_jobs)
606        )
607
608        # This version information (Win32, x64, Debug, Release, Release_Debug seems to be
609        # required for Visual Studio to understand that it needs to generate an NMAKE
610        # project. Do not modify without knowing what you are doing.
611        debug_variants = ["debug|Win32"] + ["debug|x64"]
612        release_variants = ["release|Win32"] + ["release|x64"]
613        release_debug_variants = ["release_debug|Win32"] + ["release_debug|x64"]
614        variants = debug_variants + release_variants + release_debug_variants
615        debug_targets = ["bin\\godot.windows.tools.32.exe"] + ["bin\\godot.windows.tools.64.exe"]
616        release_targets = ["bin\\godot.windows.opt.32.exe"] + ["bin\\godot.windows.opt.64.exe"]
617        release_debug_targets = ["bin\\godot.windows.opt.tools.32.exe"] + ["bin\\godot.windows.opt.tools.64.exe"]
618        targets = debug_targets + release_targets + release_debug_targets
619        if not env.get("MSVS"):
620            env["MSVS"]["PROJECTSUFFIX"] = ".vcxproj"
621            env["MSVS"]["SOLUTIONSUFFIX"] = ".sln"
622        env.MSVSProject(
623            target=["#godot" + env["MSVSPROJECTSUFFIX"]],
624            incs=env.vs_incs,
625            srcs=env.vs_srcs,
626            runfile=targets,
627            buildtarget=targets,
628            auto_build_solution=1,
629            variant=variants,
630        )
631    else:
632        print(
633            "Could not locate Visual Studio batch file for setting up the build environment. Not generating VS project."
634        )
635
636
637def precious_program(env, program, sources, **args):
638    program = env.ProgramOriginal(program, sources, **args)
639    env.Precious(program)
640    return program
641
642
643def add_shared_library(env, name, sources, **args):
644    library = env.SharedLibrary(name, sources, **args)
645    env.NoCache(library)
646    return library
647
648
649def add_library(env, name, sources, **args):
650    library = env.Library(name, sources, **args)
651    env.NoCache(library)
652    return library
653
654
655def add_program(env, name, sources, **args):
656    program = env.Program(name, sources, **args)
657    env.NoCache(program)
658    return program
659
660
661def CommandNoCache(env, target, sources, command, **args):
662    result = env.Command(target, sources, command, **args)
663    env.NoCache(result)
664    return result
665
666
667def detect_darwin_sdk_path(platform, env):
668    sdk_name = ""
669    if platform == "osx":
670        sdk_name = "macosx"
671        var_name = "MACOS_SDK_PATH"
672    elif platform == "iphone":
673        sdk_name = "iphoneos"
674        var_name = "IPHONESDK"
675    elif platform == "iphonesimulator":
676        sdk_name = "iphonesimulator"
677        var_name = "IPHONESDK"
678    else:
679        raise Exception("Invalid platform argument passed to detect_darwin_sdk_path")
680
681    if not env[var_name]:
682        try:
683            sdk_path = decode_utf8(subprocess.check_output(["xcrun", "--sdk", sdk_name, "--show-sdk-path"]).strip())
684            if sdk_path:
685                env[var_name] = sdk_path
686        except (subprocess.CalledProcessError, OSError):
687            print("Failed to find SDK path while running xcrun --sdk {} --show-sdk-path.".format(sdk_name))
688            raise
689
690
691def get_compiler_version(env):
692    """
693    Returns an array of version numbers as ints: [major, minor, patch].
694    The return array should have at least two values (major, minor).
695    """
696    if not env.msvc:
697        # Not using -dumpversion as some GCC distros only return major, and
698        # Clang used to return hardcoded 4.2.1: # https://reviews.llvm.org/D56803
699        try:
700            version = decode_utf8(subprocess.check_output([env.subst(env["CXX"]), "--version"]).strip())
701        except (subprocess.CalledProcessError, OSError):
702            print("Couldn't parse CXX environment variable to infer compiler version.")
703            return None
704    else:  # TODO: Implement for MSVC
705        return None
706    match = re.search("[0-9]+\.[0-9.]+", version)
707    if match is not None:
708        return list(map(int, match.group().split(".")))
709    else:
710        return None
711
712
713def using_gcc(env):
714    return "gcc" in os.path.basename(env["CC"])
715
716
717def using_clang(env):
718    return "clang" in os.path.basename(env["CC"])
719
720
721def show_progress(env):
722    import sys
723    from SCons.Script import Progress, Command, AlwaysBuild
724
725    screen = sys.stdout
726    # Progress reporting is not available in non-TTY environments since it
727    # messes with the output (for example, when writing to a file)
728    show_progress = env["progress"] and sys.stdout.isatty()
729    node_count_data = {
730        "count": 0,
731        "max": 0,
732        "interval": 1,
733        "fname": str(env.Dir("#")) + "/.scons_node_count",
734    }
735
736    import time, math
737
738    class cache_progress:
739        # The default is 1 GB cache and 12 hours half life
740        def __init__(self, path=None, limit=1073741824, half_life=43200):
741            self.path = path
742            self.limit = limit
743            self.exponent_scale = math.log(2) / half_life
744            if env["verbose"] and path != None:
745                screen.write(
746                    "Current cache limit is {} (used: {})\n".format(
747                        self.convert_size(limit), self.convert_size(self.get_size(path))
748                    )
749                )
750            self.delete(self.file_list())
751
752        def __call__(self, node, *args, **kw):
753            if show_progress:
754                # Print the progress percentage
755                node_count_data["count"] += node_count_data["interval"]
756                node_count = node_count_data["count"]
757                node_count_max = node_count_data["max"]
758                if node_count_max > 0 and node_count <= node_count_max:
759                    screen.write("\r[%3d%%] " % (node_count * 100 / node_count_max))
760                    screen.flush()
761                elif node_count_max > 0 and node_count > node_count_max:
762                    screen.write("\r[100%] ")
763                    screen.flush()
764                else:
765                    screen.write("\r[Initial build] ")
766                    screen.flush()
767
768        def delete(self, files):
769            if len(files) == 0:
770                return
771            if env["verbose"]:
772                # Utter something
773                screen.write("\rPurging %d %s from cache...\n" % (len(files), len(files) > 1 and "files" or "file"))
774            [os.remove(f) for f in files]
775
776        def file_list(self):
777            if self.path is None:
778                # Nothing to do
779                return []
780            # Gather a list of (filename, (size, atime)) within the
781            # cache directory
782            file_stat = [(x, os.stat(x)[6:8]) for x in glob.glob(os.path.join(self.path, "*", "*"))]
783            if file_stat == []:
784                # Nothing to do
785                return []
786            # Weight the cache files by size (assumed to be roughly
787            # proportional to the recompilation time) times an exponential
788            # decay since the ctime, and return a list with the entries
789            # (filename, size, weight).
790            current_time = time.time()
791            file_stat = [(x[0], x[1][0], (current_time - x[1][1])) for x in file_stat]
792            # Sort by the most recently accessed files (most sensible to keep) first
793            file_stat.sort(key=lambda x: x[2])
794            # Search for the first entry where the storage limit is
795            # reached
796            sum, mark = 0, None
797            for i, x in enumerate(file_stat):
798                sum += x[1]
799                if sum > self.limit:
800                    mark = i
801                    break
802            if mark is None:
803                return []
804            else:
805                return [x[0] for x in file_stat[mark:]]
806
807        def convert_size(self, size_bytes):
808            if size_bytes == 0:
809                return "0 bytes"
810            size_name = ("bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB")
811            i = int(math.floor(math.log(size_bytes, 1024)))
812            p = math.pow(1024, i)
813            s = round(size_bytes / p, 2)
814            return "%s %s" % (int(s) if i == 0 else s, size_name[i])
815
816        def get_size(self, start_path="."):
817            total_size = 0
818            for dirpath, dirnames, filenames in os.walk(start_path):
819                for f in filenames:
820                    fp = os.path.join(dirpath, f)
821                    total_size += os.path.getsize(fp)
822            return total_size
823
824    def progress_finish(target, source, env):
825        with open(node_count_data["fname"], "w") as f:
826            f.write("%d\n" % node_count_data["count"])
827        progressor.delete(progressor.file_list())
828
829    try:
830        with open(node_count_data["fname"]) as f:
831            node_count_data["max"] = int(f.readline())
832    except:
833        pass
834
835    cache_directory = os.environ.get("SCONS_CACHE")
836    # Simple cache pruning, attached to SCons' progress callback. Trim the
837    # cache directory to a size not larger than cache_limit.
838    cache_limit = float(os.getenv("SCONS_CACHE_LIMIT", 1024)) * 1024 * 1024
839    progressor = cache_progress(cache_directory, cache_limit)
840    Progress(progressor, interval=node_count_data["interval"])
841
842    progress_finish_command = Command("progress_finish", [], progress_finish)
843    AlwaysBuild(progress_finish_command)
844
845
846def dump(env):
847    # Dumps latest build information for debugging purposes and external tools.
848    from json import dump
849
850    def non_serializable(obj):
851        return "<<non-serializable: %s>>" % (qualname(type(obj)))
852
853    with open(".scons_env.json", "w") as f:
854        dump(env.Dictionary(), f, indent=4, default=non_serializable)
855