Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
godotengine
GitHub Repository: godotengine/godot
Path: blob/master/methods.py
9819 views
1
import atexit
2
import contextlib
3
import glob
4
import math
5
import os
6
import re
7
import subprocess
8
import sys
9
import textwrap
10
import zlib
11
from collections import OrderedDict
12
from io import StringIO, TextIOBase
13
from pathlib import Path
14
from typing import Generator, List, Optional, Union, cast
15
16
from misc.utility.color import print_error, print_info, print_warning
17
from platform_methods import detect_arch
18
19
# Get the "Godot" folder name ahead of time
20
base_folder = Path(__file__).resolve().parent
21
22
compiler_version_cache = None
23
24
# Listing all the folders we have converted
25
# for SCU in scu_builders.py
26
_scu_folders = set()
27
28
29
def set_scu_folders(scu_folders):
30
global _scu_folders
31
_scu_folders = scu_folders
32
33
34
def add_source_files_orig(self, sources, files, allow_gen=False):
35
# Convert string to list of absolute paths (including expanding wildcard)
36
if isinstance(files, str):
37
# Exclude .gen.cpp files from globbing, to avoid including obsolete ones.
38
# They should instead be added manually.
39
skip_gen_cpp = "*" in files
40
files = self.Glob(files)
41
if skip_gen_cpp and not allow_gen:
42
files = [f for f in files if not str(f).endswith(".gen.cpp")]
43
44
# Add each path as compiled Object following environment (self) configuration
45
for path in files:
46
obj = self.Object(path)
47
if obj in sources:
48
print_warning('Object "{}" already included in environment sources.'.format(obj))
49
continue
50
sources.append(obj)
51
52
53
def add_source_files_scu(self, sources, files, allow_gen=False):
54
if self["scu_build"] and isinstance(files, str):
55
if "*." not in files:
56
return False
57
58
# If the files are in a subdirectory, we want to create the scu gen
59
# files inside this subdirectory.
60
subdir = os.path.dirname(files)
61
subdir = subdir if subdir == "" else subdir + "/"
62
section_name = self.Dir(subdir).tpath
63
section_name = section_name.replace("\\", "/") # win32
64
# if the section name is in the hash table?
65
# i.e. is it part of the SCU build?
66
global _scu_folders
67
if section_name not in (_scu_folders):
68
return False
69
70
# Add all the gen.cpp files in the SCU directory
71
add_source_files_orig(self, sources, subdir + ".scu/scu_*.gen.cpp", True)
72
return True
73
return False
74
75
76
# Either builds the folder using the SCU system,
77
# or reverts to regular build.
78
def add_source_files(self, sources, files, allow_gen=False):
79
if not add_source_files_scu(self, sources, files, allow_gen):
80
# Wraps the original function when scu build is not active.
81
add_source_files_orig(self, sources, files, allow_gen)
82
return False
83
return True
84
85
86
def redirect_emitter(target, source, env):
87
"""
88
Emitter to automatically redirect object/library build files to the `bin/obj` directory,
89
retaining subfolder structure. External build files will attempt to retain subfolder
90
structure relative to their environment's parent directory, sorted under `bin/obj/external`.
91
If `redirect_build_objects` is `False`, or an external build file isn't relative to the
92
passed environment, this emitter does nothing.
93
"""
94
if not env["redirect_build_objects"]:
95
return target, source
96
97
redirected_targets = []
98
for item in target:
99
if base_folder in (path := Path(item.get_abspath()).resolve()).parents:
100
item = env.File(f"#bin/obj/{path.relative_to(base_folder)}")
101
elif (alt_base := Path(env.Dir(".").get_abspath()).resolve().parent) in path.parents:
102
item = env.File(f"#bin/obj/external/{path.relative_to(alt_base)}")
103
else:
104
print_warning(f'Failed to redirect "{path}"')
105
redirected_targets.append(item)
106
return redirected_targets, source
107
108
109
def disable_warnings(self):
110
# 'self' is the environment
111
if self.msvc and not using_clang(self):
112
self["WARNLEVEL"] = "/w"
113
else:
114
self["WARNLEVEL"] = "-w"
115
116
117
def force_optimization_on_debug(self):
118
# 'self' is the environment
119
if self["target"] == "template_release":
120
return
121
elif self.msvc:
122
self["OPTIMIZELEVEL"] = "/O2"
123
else:
124
self["OPTIMIZELEVEL"] = "-O3"
125
126
127
def add_module_version_string(self, s):
128
self.module_version_string += "." + s
129
130
131
def get_version_info(module_version_string="", silent=False):
132
build_name = "custom_build"
133
if os.getenv("BUILD_NAME") is not None:
134
build_name = str(os.getenv("BUILD_NAME"))
135
if not silent:
136
print_info(f"Using custom build name: '{build_name}'.")
137
138
import version
139
140
version_info = {
141
"short_name": str(version.short_name),
142
"name": str(version.name),
143
"major": int(version.major),
144
"minor": int(version.minor),
145
"patch": int(version.patch),
146
"status": str(version.status),
147
"build": str(build_name),
148
"module_config": str(version.module_config) + module_version_string,
149
"website": str(version.website),
150
"docs_branch": str(version.docs),
151
}
152
153
# For dev snapshots (alpha, beta, RC, etc.) we do not commit status change to Git,
154
# so this define provides a way to override it without having to modify the source.
155
if os.getenv("GODOT_VERSION_STATUS") is not None:
156
version_info["status"] = str(os.getenv("GODOT_VERSION_STATUS"))
157
if not silent:
158
print_info(f"Using version status '{version_info['status']}', overriding the original '{version.status}'.")
159
160
return version_info
161
162
163
def get_git_info():
164
os.chdir(base_folder)
165
166
# Parse Git hash if we're in a Git repo.
167
git_hash = ""
168
git_folder = ".git"
169
170
if os.path.isfile(".git"):
171
with open(".git", "r", encoding="utf-8") as file:
172
module_folder = file.readline().strip()
173
if module_folder.startswith("gitdir: "):
174
git_folder = module_folder[8:]
175
176
if os.path.isfile(os.path.join(git_folder, "HEAD")):
177
with open(os.path.join(git_folder, "HEAD"), "r", encoding="utf8") as file:
178
head = file.readline().strip()
179
if head.startswith("ref: "):
180
ref = head[5:]
181
# If this directory is a Git worktree instead of a root clone.
182
parts = git_folder.split("/")
183
if len(parts) > 2 and parts[-2] == "worktrees":
184
git_folder = "/".join(parts[0:-2])
185
head = os.path.join(git_folder, ref)
186
packedrefs = os.path.join(git_folder, "packed-refs")
187
if os.path.isfile(head):
188
with open(head, "r", encoding="utf-8") as file:
189
git_hash = file.readline().strip()
190
elif os.path.isfile(packedrefs):
191
# Git may pack refs into a single file. This code searches .git/packed-refs file for the current ref's hash.
192
# https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-pack-refs.html
193
for line in open(packedrefs, "r", encoding="utf-8").read().splitlines():
194
if line.startswith("#"):
195
continue
196
(line_hash, line_ref) = line.split(" ")
197
if ref == line_ref:
198
git_hash = line_hash
199
break
200
else:
201
git_hash = head
202
203
# Get the UNIX timestamp of the build commit.
204
git_timestamp = 0
205
if os.path.exists(".git"):
206
try:
207
git_timestamp = subprocess.check_output(
208
["git", "log", "-1", "--pretty=format:%ct", "--no-show-signature", git_hash], encoding="utf-8"
209
)
210
except (subprocess.CalledProcessError, OSError):
211
# `git` not found in PATH.
212
pass
213
214
return {
215
"git_hash": git_hash,
216
"git_timestamp": git_timestamp,
217
}
218
219
220
def get_cmdline_bool(option, default):
221
"""We use `ARGUMENTS.get()` to check if options were manually overridden on the command line,
222
and SCons' _text2bool helper to convert them to booleans, otherwise they're handled as strings.
223
"""
224
from SCons.Script import ARGUMENTS
225
from SCons.Variables.BoolVariable import _text2bool
226
227
cmdline_val = ARGUMENTS.get(option)
228
if cmdline_val is not None:
229
return _text2bool(cmdline_val)
230
else:
231
return default
232
233
234
def detect_modules(search_path, recursive=False):
235
"""Detects and collects a list of C++ modules at specified path
236
237
`search_path` - a directory path containing modules. The path may point to
238
a single module, which may have other nested modules. A module must have
239
"register_types.h", "SCsub", "config.py" files created to be detected.
240
241
`recursive` - if `True`, then all subdirectories are searched for modules as
242
specified by the `search_path`, otherwise collects all modules under the
243
`search_path` directory. If the `search_path` is a module, it is collected
244
in all cases.
245
246
Returns an `OrderedDict` with module names as keys, and directory paths as
247
values. If a path is relative, then it is a built-in module. If a path is
248
absolute, then it is a custom module collected outside of the engine source.
249
"""
250
modules = OrderedDict()
251
252
def add_module(path):
253
module_name = os.path.basename(path)
254
module_path = path.replace("\\", "/") # win32
255
modules[module_name] = module_path
256
257
def is_engine(path):
258
# Prevent recursively detecting modules in self and other
259
# Godot sources when using `custom_modules` build option.
260
version_path = os.path.join(path, "version.py")
261
if os.path.exists(version_path):
262
with open(version_path, "r", encoding="utf-8") as f:
263
if 'short_name = "godot"' in f.read():
264
return True
265
return False
266
267
def get_files(path):
268
files = glob.glob(os.path.join(path, "*"))
269
# Sort so that `register_module_types` does not change that often,
270
# and plugins are registered in alphabetic order as well.
271
files.sort()
272
return files
273
274
if not recursive:
275
if is_module(search_path):
276
add_module(search_path)
277
for path in get_files(search_path):
278
if is_engine(path):
279
continue
280
if is_module(path):
281
add_module(path)
282
else:
283
to_search = [search_path]
284
while to_search:
285
path = to_search.pop()
286
if is_module(path):
287
add_module(path)
288
for child in get_files(path):
289
if not os.path.isdir(child):
290
continue
291
if is_engine(child):
292
continue
293
to_search.insert(0, child)
294
return modules
295
296
297
def is_module(path):
298
if not os.path.isdir(path):
299
return False
300
must_exist = ["register_types.h", "SCsub", "config.py"]
301
for f in must_exist:
302
if not os.path.exists(os.path.join(path, f)):
303
return False
304
return True
305
306
307
def convert_custom_modules_path(path):
308
if not path:
309
return path
310
path = os.path.realpath(os.path.expanduser(os.path.expandvars(path)))
311
err_msg = "Build option 'custom_modules' must %s"
312
if not os.path.isdir(path):
313
raise ValueError(err_msg % "point to an existing directory.")
314
if path == os.path.realpath("modules"):
315
raise ValueError(err_msg % "be a directory other than built-in `modules` directory.")
316
return path
317
318
319
def module_add_dependencies(self, module, dependencies, optional=False):
320
"""
321
Adds dependencies for a given module.
322
Meant to be used in module `can_build` methods.
323
"""
324
if module not in self.module_dependencies:
325
self.module_dependencies[module] = [[], []]
326
if optional:
327
self.module_dependencies[module][1].extend(dependencies)
328
else:
329
self.module_dependencies[module][0].extend(dependencies)
330
331
332
def module_check_dependencies(self, module):
333
"""
334
Checks if module dependencies are enabled for a given module,
335
and prints a warning if they aren't.
336
Meant to be used in module `can_build` methods.
337
Returns a boolean (True if dependencies are satisfied).
338
"""
339
missing_deps = set()
340
required_deps = self.module_dependencies[module][0] if module in self.module_dependencies else []
341
for dep in required_deps:
342
opt = "module_{}_enabled".format(dep)
343
if opt not in self or not self[opt] or not module_check_dependencies(self, dep):
344
missing_deps.add(dep)
345
346
if missing_deps:
347
if module not in self.disabled_modules:
348
print_warning(
349
"Disabling '{}' module as the following dependencies are not satisfied: {}".format(
350
module, ", ".join(missing_deps)
351
)
352
)
353
self.disabled_modules.add(module)
354
return False
355
else:
356
return True
357
358
359
def sort_module_list(env):
360
deps = {k: v[0] + list(filter(lambda x: x in env.module_list, v[1])) for k, v in env.module_dependencies.items()}
361
362
frontier = list(env.module_list.keys())
363
explored = []
364
while len(frontier):
365
cur = frontier.pop()
366
deps_list = deps[cur] if cur in deps else []
367
if len(deps_list) and any([d not in explored for d in deps_list]):
368
# Will explore later, after its dependencies
369
frontier.insert(0, cur)
370
continue
371
explored.append(cur)
372
for k in explored:
373
env.module_list.move_to_end(k)
374
375
376
def use_windows_spawn_fix(self, platform=None):
377
if os.name != "nt":
378
return # not needed, only for windows
379
380
def mySubProcess(cmdline, env):
381
startupinfo = subprocess.STARTUPINFO()
382
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
383
popen_args = {
384
"stdin": subprocess.PIPE,
385
"stdout": subprocess.PIPE,
386
"stderr": subprocess.PIPE,
387
"startupinfo": startupinfo,
388
"shell": False,
389
"env": env,
390
}
391
popen_args["text"] = True
392
proc = subprocess.Popen(cmdline, **popen_args)
393
_, err = proc.communicate()
394
rv = proc.wait()
395
if rv:
396
print_error(err)
397
elif len(err) > 0 and not err.isspace():
398
print(err)
399
return rv
400
401
def mySpawn(sh, escape, cmd, args, env):
402
# Used by TEMPFILE.
403
if cmd == "del":
404
os.remove(args[1])
405
return 0
406
407
newargs = " ".join(args[1:])
408
cmdline = cmd + " " + newargs
409
410
rv = 0
411
env = {str(key): str(value) for key, value in iter(env.items())}
412
rv = mySubProcess(cmdline, env)
413
414
return rv
415
416
self["SPAWN"] = mySpawn
417
418
419
def no_verbose(env):
420
from misc.utility.color import Ansi, is_stdout_color
421
422
colors = [Ansi.BLUE, Ansi.BOLD, Ansi.REGULAR, Ansi.RESET] if is_stdout_color() else ["", "", "", ""]
423
424
# There is a space before "..." to ensure that source file names can be
425
# Ctrl + clicked in the VS Code terminal.
426
compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors)
427
java_compile_source_message = "{}Compiling {}$SOURCE{} ...{}".format(*colors)
428
compile_shared_source_message = "{}Compiling shared {}$SOURCE{} ...{}".format(*colors)
429
link_program_message = "{}Linking Program {}$TARGET{} ...{}".format(*colors)
430
link_library_message = "{}Linking Static Library {}$TARGET{} ...{}".format(*colors)
431
ranlib_library_message = "{}Ranlib Library {}$TARGET{} ...{}".format(*colors)
432
link_shared_library_message = "{}Linking Shared Library {}$TARGET{} ...{}".format(*colors)
433
java_library_message = "{}Creating Java Archive {}$TARGET{} ...{}".format(*colors)
434
compiled_resource_message = "{}Creating Compiled Resource {}$TARGET{} ...{}".format(*colors)
435
zip_archive_message = "{}Archiving {}$TARGET{} ...{}".format(*colors)
436
generated_file_message = "{}Generating {}$TARGET{} ...{}".format(*colors)
437
438
env["CXXCOMSTR"] = compile_source_message
439
env["CCCOMSTR"] = compile_source_message
440
env["SHCCCOMSTR"] = compile_shared_source_message
441
env["SHCXXCOMSTR"] = compile_shared_source_message
442
env["ARCOMSTR"] = link_library_message
443
env["RANLIBCOMSTR"] = ranlib_library_message
444
env["SHLINKCOMSTR"] = link_shared_library_message
445
env["LINKCOMSTR"] = link_program_message
446
env["JARCOMSTR"] = java_library_message
447
env["JAVACCOMSTR"] = java_compile_source_message
448
env["RCCOMSTR"] = compiled_resource_message
449
env["ZIPCOMSTR"] = zip_archive_message
450
env["GENCOMSTR"] = generated_file_message
451
452
453
def detect_visual_c_compiler_version(tools_env):
454
# tools_env is the variable scons uses to call tools that execute tasks, SCons's env['ENV'] that executes tasks...
455
# (see the SCons documentation for more information on what it does)...
456
# in order for this function to be well encapsulated i choose to force it to receive SCons's TOOLS env (env['ENV']
457
# and not scons setup environment (env)... so make sure you call the right environment on it or it will fail to detect
458
# the proper vc version that will be called
459
460
# There is no flag to give to visual c compilers to set the architecture, i.e. scons arch argument (x86_32, x86_64, arm64, etc.).
461
# There are many different cl.exe files that are run, and each one compiles & links to a different architecture
462
# As far as I know, the only way to figure out what compiler will be run when Scons calls cl.exe via Program()
463
# is to check the PATH variable and figure out which one will be called first. Code below does that and returns:
464
# the following string values:
465
466
# "" Compiler not detected
467
# "amd64" Native 64 bit compiler
468
# "amd64_x86" 64 bit Cross Compiler for 32 bit
469
# "x86" Native 32 bit compiler
470
# "x86_amd64" 32 bit Cross Compiler for 64 bit
471
472
# There are other architectures, but Godot does not support them currently, so this function does not detect arm/amd64_arm
473
# and similar architectures/compilers
474
475
# Set chosen compiler to "not detected"
476
vc_chosen_compiler_index = -1
477
vc_chosen_compiler_str = ""
478
479
# VS 2017 and newer should set VCTOOLSINSTALLDIR
480
if "VCTOOLSINSTALLDIR" in tools_env:
481
# Newer versions have a different path available
482
vc_amd64_compiler_detection_index = (
483
tools_env["PATH"].upper().find(tools_env["VCTOOLSINSTALLDIR"].upper() + "BIN\\HOSTX64\\X64;")
484
)
485
if vc_amd64_compiler_detection_index > -1:
486
vc_chosen_compiler_index = vc_amd64_compiler_detection_index
487
vc_chosen_compiler_str = "amd64"
488
489
vc_amd64_x86_compiler_detection_index = (
490
tools_env["PATH"].upper().find(tools_env["VCTOOLSINSTALLDIR"].upper() + "BIN\\HOSTX64\\X86;")
491
)
492
if vc_amd64_x86_compiler_detection_index > -1 and (
493
vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_amd64_x86_compiler_detection_index
494
):
495
vc_chosen_compiler_index = vc_amd64_x86_compiler_detection_index
496
vc_chosen_compiler_str = "amd64_x86"
497
498
vc_x86_compiler_detection_index = (
499
tools_env["PATH"].upper().find(tools_env["VCTOOLSINSTALLDIR"].upper() + "BIN\\HOSTX86\\X86;")
500
)
501
if vc_x86_compiler_detection_index > -1 and (
502
vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_x86_compiler_detection_index
503
):
504
vc_chosen_compiler_index = vc_x86_compiler_detection_index
505
vc_chosen_compiler_str = "x86"
506
507
vc_x86_amd64_compiler_detection_index = (
508
tools_env["PATH"].upper().find(tools_env["VCTOOLSINSTALLDIR"].upper() + "BIN\\HOSTX86\\X64;")
509
)
510
if vc_x86_amd64_compiler_detection_index > -1 and (
511
vc_chosen_compiler_index == -1 or vc_chosen_compiler_index > vc_x86_amd64_compiler_detection_index
512
):
513
vc_chosen_compiler_str = "x86_amd64"
514
515
return vc_chosen_compiler_str
516
517
518
def find_visual_c_batch_file(env):
519
# TODO: We should investigate if we can avoid relying on SCons internals here.
520
from SCons.Tool.MSCommon.vc import find_batch_file, find_vc_pdir, get_default_version, get_host_target
521
522
msvc_version = get_default_version(env)
523
524
# Syntax changed in SCons 4.4.0.
525
if env.scons_version >= (4, 4, 0):
526
(host_platform, target_platform, _) = get_host_target(env, msvc_version)
527
else:
528
(host_platform, target_platform, _) = get_host_target(env)
529
530
if env.scons_version < (4, 6, 0):
531
return find_batch_file(env, msvc_version, host_platform, target_platform)[0]
532
533
# SCons 4.6.0+ removed passing env, so we need to get the product_dir ourselves first,
534
# then pass that as the last param instead of env as the first param as before.
535
# Param names need to be explicit, as they were shuffled around in SCons 4.8.0.
536
product_dir = find_vc_pdir(msvc_version=msvc_version, env=env)
537
538
return find_batch_file(msvc_version, host_platform, target_platform, product_dir)[0]
539
540
541
def generate_cpp_hint_file(filename):
542
if os.path.isfile(filename):
543
# Don't overwrite an existing hint file since the user may have customized it.
544
pass
545
else:
546
try:
547
with open(filename, "w", encoding="utf-8", newline="\n") as fd:
548
fd.write("#define GDCLASS(m_class, m_inherits)\n")
549
for name in ["GDVIRTUAL", "EXBIND", "MODBIND"]:
550
for count in range(13):
551
for suffix in ["", "R", "C", "RC"]:
552
fd.write(f"#define {name}{count}{suffix}(")
553
if "R" in suffix:
554
fd.write("m_ret, ")
555
fd.write("m_name")
556
for idx in range(1, count + 1):
557
fd.write(f", type{idx}")
558
fd.write(")\n")
559
560
except OSError:
561
print_warning("Could not write cpp.hint file.")
562
563
564
def glob_recursive(pattern, node="."):
565
from SCons import Node
566
from SCons.Script import Glob
567
568
results = []
569
for f in Glob(str(node) + "/*", source=True):
570
if type(f) is Node.FS.Dir:
571
results += glob_recursive(pattern, f)
572
results += Glob(str(node) + "/" + pattern, source=True)
573
return results
574
575
576
def precious_program(env, program, sources, **args):
577
program = env.Program(program, sources, **args)
578
env.Precious(program)
579
return program
580
581
582
def add_shared_library(env, name, sources, **args):
583
library = env.SharedLibrary(name, sources, **args)
584
env.NoCache(library)
585
return library
586
587
588
def add_library(env, name, sources, **args):
589
library = env.Library(name, sources, **args)
590
env.NoCache(library)
591
return library
592
593
594
def add_program(env, name, sources, **args):
595
program = env.Program(name, sources, **args)
596
env.NoCache(program)
597
return program
598
599
600
def CommandNoCache(env, target, sources, command, **args):
601
result = env.Command(target, sources, command, **args)
602
env.NoCache(result)
603
return result
604
605
606
def Run(env, function, comstr="$GENCOMSTR"):
607
from SCons.Script import Action
608
609
return Action(function, comstr)
610
611
612
def detect_darwin_toolchain_path(env):
613
var_name = "APPLE_TOOLCHAIN_PATH"
614
if not env[var_name]:
615
try:
616
xcode_path = subprocess.check_output(["xcode-select", "-p"]).strip().decode("utf-8")
617
if xcode_path:
618
env[var_name] = xcode_path + "/Toolchains/XcodeDefault.xctoolchain"
619
except (subprocess.CalledProcessError, OSError):
620
print_error("Failed to find SDK path while running 'xcode-select -p'.")
621
raise
622
623
624
def detect_darwin_sdk_path(platform, env):
625
sdk_name = ""
626
627
if platform == "macos":
628
sdk_name = "macosx"
629
var_name = "MACOS_SDK_PATH"
630
631
elif platform == "ios":
632
sdk_name = "iphoneos"
633
var_name = "IOS_SDK_PATH"
634
635
elif platform == "iossimulator":
636
sdk_name = "iphonesimulator"
637
var_name = "IOS_SDK_PATH"
638
639
elif platform == "visionos":
640
sdk_name = "xros"
641
var_name = "VISIONOS_SDK_PATH"
642
643
elif platform == "visionossimulator":
644
sdk_name = "xrsimulator"
645
var_name = "VISIONOS_SDK_PATH"
646
647
else:
648
raise Exception("Invalid platform argument passed to detect_darwin_sdk_path")
649
650
if not env[var_name]:
651
try:
652
sdk_path = subprocess.check_output(["xcrun", "--sdk", sdk_name, "--show-sdk-path"]).strip().decode("utf-8")
653
if sdk_path:
654
env[var_name] = sdk_path
655
except (subprocess.CalledProcessError, OSError):
656
print_error("Failed to find SDK path while running 'xcrun --sdk {} --show-sdk-path'.".format(sdk_name))
657
raise
658
659
660
def is_apple_clang(env):
661
import shlex
662
663
if env["platform"] not in ["macos", "ios"]:
664
return False
665
if not using_clang(env):
666
return False
667
try:
668
version = (
669
subprocess.check_output(shlex.split(env.subst(env["CXX"]), posix=False) + ["--version"])
670
.strip()
671
.decode("utf-8")
672
)
673
except (subprocess.CalledProcessError, OSError):
674
print_warning("Couldn't parse CXX environment variable to infer compiler version.")
675
return False
676
return version.startswith("Apple")
677
678
679
def get_compiler_version(env):
680
"""
681
Returns a dictionary with various version information:
682
683
- major, minor, patch: Version following semantic versioning system
684
- metadata1, metadata2: Extra information
685
- date: Date of the build
686
"""
687
688
global compiler_version_cache
689
if compiler_version_cache is not None:
690
return compiler_version_cache
691
692
import shlex
693
694
ret = {
695
"major": -1,
696
"minor": -1,
697
"patch": -1,
698
"metadata1": "",
699
"metadata2": "",
700
"date": "",
701
"apple_major": -1,
702
"apple_minor": -1,
703
"apple_patch1": -1,
704
"apple_patch2": -1,
705
"apple_patch3": -1,
706
}
707
708
if env.msvc and not using_clang(env):
709
try:
710
# FIXME: `-latest` works for most cases, but there are edge-cases where this would
711
# benefit from a more nuanced search.
712
# https://github.com/godotengine/godot/pull/91069#issuecomment-2358956731
713
# https://github.com/godotengine/godot/pull/91069#issuecomment-2380836341
714
args = [
715
env["VSWHERE"],
716
"-latest",
717
"-prerelease",
718
"-products",
719
"*",
720
"-requires",
721
"Microsoft.Component.MSBuild",
722
"-utf8",
723
]
724
version = subprocess.check_output(args, encoding="utf-8").strip()
725
for line in version.splitlines():
726
split = line.split(":", 1)
727
if split[0] == "catalog_productDisplayVersion":
728
sem_ver = split[1].split(".")
729
ret["major"] = int(sem_ver[0])
730
ret["minor"] = int(sem_ver[1])
731
ret["patch"] = int(sem_ver[2].split()[0])
732
# Could potentially add section for determining preview version, but
733
# that can wait until metadata is actually used for something.
734
if split[0] == "catalog_buildVersion":
735
ret["metadata1"] = split[1]
736
except (subprocess.CalledProcessError, OSError):
737
print_warning("Couldn't find vswhere to determine compiler version.")
738
return update_compiler_version_cache(ret)
739
740
# Not using -dumpversion as some GCC distros only return major, and
741
# Clang used to return hardcoded 4.2.1: # https://reviews.llvm.org/D56803
742
try:
743
version = subprocess.check_output(
744
shlex.split(env.subst(env["CXX"]), posix=False) + ["--version"], shell=(os.name == "nt"), encoding="utf-8"
745
).strip()
746
except (subprocess.CalledProcessError, OSError):
747
print_warning("Couldn't parse CXX environment variable to infer compiler version.")
748
return update_compiler_version_cache(ret)
749
750
match = re.search(
751
r"(?:(?<=version )|(?<=\) )|(?<=^))"
752
r"(?P<major>\d+)"
753
r"(?:\.(?P<minor>\d*))?"
754
r"(?:\.(?P<patch>\d*))?"
755
r"(?:-(?P<metadata1>[0-9a-zA-Z-]*))?"
756
r"(?:\+(?P<metadata2>[0-9a-zA-Z-]*))?"
757
r"(?: (?P<date>[0-9]{8}|[0-9]{6})(?![0-9a-zA-Z]))?",
758
version,
759
)
760
if match is not None:
761
for key, value in match.groupdict().items():
762
if value is not None:
763
ret[key] = value
764
765
match_apple = re.search(
766
r"(?:(?<=clang-)|(?<=\) )|(?<=^))"
767
r"(?P<apple_major>\d+)"
768
r"(?:\.(?P<apple_minor>\d*))?"
769
r"(?:\.(?P<apple_patch1>\d*))?"
770
r"(?:\.(?P<apple_patch2>\d*))?"
771
r"(?:\.(?P<apple_patch3>\d*))?",
772
version,
773
)
774
if match_apple is not None:
775
for key, value in match_apple.groupdict().items():
776
if value is not None:
777
ret[key] = value
778
779
# Transform semantic versioning to integers
780
for key in [
781
"major",
782
"minor",
783
"patch",
784
"apple_major",
785
"apple_minor",
786
"apple_patch1",
787
"apple_patch2",
788
"apple_patch3",
789
]:
790
ret[key] = int(ret[key] or -1)
791
return update_compiler_version_cache(ret)
792
793
794
def update_compiler_version_cache(value):
795
global compiler_version_cache
796
compiler_version_cache = value
797
return value
798
799
800
def using_gcc(env):
801
return "gcc" in os.path.basename(env["CC"])
802
803
804
def using_clang(env):
805
return "clang" in os.path.basename(env["CC"])
806
807
808
def using_emcc(env):
809
return "emcc" in os.path.basename(env["CC"])
810
811
812
def show_progress(env):
813
# Ninja has its own progress/tracking tool that clashes with ours.
814
if env["ninja"]:
815
return
816
817
NODE_COUNT_FILENAME = base_folder / ".scons_node_count"
818
819
class ShowProgress:
820
def __init__(self):
821
self.count = 0
822
self.max = 0
823
try:
824
with open(NODE_COUNT_FILENAME, "r", encoding="utf-8") as f:
825
self.max = int(f.readline())
826
except OSError:
827
pass
828
829
# Progress reporting is not available in non-TTY environments since it
830
# messes with the output (for example, when writing to a file).
831
self.display = cast(bool, env["progress"] and sys.stdout.isatty())
832
if self.display and not self.max:
833
print_info("Performing initial build, progress percentage unavailable!")
834
self.display = False
835
836
def __call__(self, node, *args, **kw):
837
self.count += 1
838
if self.display:
839
percent = int(min(self.count * 100 / self.max, 100))
840
sys.stdout.write(f"\r[{percent:3d}%] ")
841
sys.stdout.flush()
842
843
from SCons.Script import Progress
844
from SCons.Script.Main import GetBuildFailures
845
846
progressor = ShowProgress()
847
Progress(progressor)
848
849
def progress_finish():
850
if GetBuildFailures() or not progressor.count:
851
return
852
try:
853
with open(NODE_COUNT_FILENAME, "w", encoding="utf-8", newline="\n") as f:
854
f.write(f"{progressor.count}\n")
855
except OSError:
856
pass
857
858
atexit.register(progress_finish)
859
860
861
def convert_size(size_bytes: int) -> str:
862
if size_bytes == 0:
863
return "0 bytes"
864
SIZE_NAMES = ["bytes", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB"]
865
index = math.floor(math.log(size_bytes, 1024))
866
power = math.pow(1024, index)
867
size = round(size_bytes / power, 2)
868
return f"{size} {SIZE_NAMES[index]}"
869
870
871
def get_size(start_path: str = ".") -> int:
872
total_size = 0
873
for dirpath, _, filenames in os.walk(start_path):
874
for file in filenames:
875
path = os.path.join(dirpath, file)
876
total_size += os.path.getsize(path)
877
return total_size
878
879
880
def clean_cache(cache_path: str, cache_limit: int, verbose: bool) -> None:
881
if not cache_limit:
882
return
883
884
files = glob.glob(os.path.join(cache_path, "*", "*"))
885
if not files:
886
return
887
888
# Store files in list of (filename, size, atime).
889
stats = []
890
for file in files:
891
try:
892
stats.append((file, *os.stat(file)[6:8]))
893
except OSError:
894
print_error(f'Failed to access cache file "{file}"; skipping.')
895
896
# Sort by most recent access (most sensible to keep) first. Search for the first entry where
897
# the cache limit is reached.
898
stats.sort(key=lambda x: x[2], reverse=True)
899
sum = 0
900
for index, stat in enumerate(stats):
901
sum += stat[1]
902
if sum > cache_limit:
903
purge = [x[0] for x in stats[index:]]
904
count = len(purge)
905
for file in purge:
906
try:
907
os.remove(file)
908
except OSError:
909
print_error(f'Failed to remove cache file "{file}"; skipping.')
910
count -= 1
911
if verbose and count:
912
print_info(f"Purged {count} file{'s' if count else ''} from cache.")
913
break
914
915
916
def prepare_cache(env) -> None:
917
cache_path = ""
918
if env["cache_path"]:
919
cache_path = cast(str, env["cache_path"])
920
elif os.environ.get("SCONS_CACHE"):
921
print_warning("Environment variable `SCONS_CACHE` is deprecated; use `cache_path` argument instead.")
922
cache_path = cast(str, os.environ.get("SCONS_CACHE"))
923
924
if not cache_path:
925
return
926
927
env.CacheDir(cache_path)
928
print(f'SCons cache enabled... (path: "{cache_path}")')
929
930
if env["cache_limit"]:
931
cache_limit = float(env["cache_limit"])
932
elif os.environ.get("SCONS_CACHE_LIMIT"):
933
print_warning("Environment variable `SCONS_CACHE_LIMIT` is deprecated; use `cache_limit` argument instead.")
934
cache_limit = float(os.getenv("SCONS_CACHE_LIMIT", "0")) / 1024 # Old method used MiB, convert to GiB
935
936
# Convert GiB to bytes; treat negative numbers as 0 (unlimited).
937
cache_limit = max(0, int(cache_limit * 1024 * 1024 * 1024))
938
if env["verbose"]:
939
print_info(
940
f"Current cache size is {convert_size(get_size(cache_path))}"
941
+ (f" (limit: {convert_size(cache_limit)})" if cache_limit else "")
942
)
943
944
atexit.register(clean_cache, cache_path, cache_limit, env["verbose"])
945
946
947
def prepare_purge(env):
948
from SCons.Script.Main import GetBuildFailures
949
950
def purge_flaky_files():
951
paths_to_keep = [env["ninja_file"]]
952
for build_failure in GetBuildFailures():
953
path = build_failure.node.path
954
if os.path.isfile(path) and path not in paths_to_keep:
955
os.remove(path)
956
957
atexit.register(purge_flaky_files)
958
959
960
def prepare_timer():
961
import time
962
963
def print_elapsed_time(time_at_start: float):
964
time_elapsed = time.time() - time_at_start
965
time_formatted = time.strftime("%H:%M:%S", time.gmtime(time_elapsed))
966
time_centiseconds = (time_elapsed % 1) * 100
967
print_info(f"Time elapsed: {time_formatted}.{time_centiseconds:02.0f}")
968
969
atexit.register(print_elapsed_time, time.time())
970
971
972
def dump(env):
973
"""
974
Dumps latest build information for debugging purposes and external tools.
975
"""
976
977
with open(".scons_env.json", "w", encoding="utf-8", newline="\n") as file:
978
file.write(env.Dump(format="json"))
979
980
981
# Custom Visual Studio project generation logic that supports any platform that has a msvs.py
982
# script, so Visual Studio can be used to run scons for any platform, with the right defines per target.
983
# Invoked with scons vsproj=yes
984
#
985
# Only platforms that opt in to vs proj generation by having a msvs.py file in the platform folder are included.
986
# Platforms with a msvs.py file will be added to the solution, but only the current active platform+target+arch
987
# will have a build configuration generated, because we only know what the right defines/includes/flags/etc are
988
# on the active build target.
989
#
990
# Platforms that don't support an editor target will have a dummy editor target that won't do anything on build,
991
# but will have the files and configuration for the windows editor target.
992
#
993
# To generate build configuration files for all platforms+targets+arch combinations, users can call
994
# scons vsproj=yes
995
# for each combination of platform+target+arch. This will generate the relevant vs project files but
996
# skip the build process. This lets project files be quickly generated even if there are build errors.
997
#
998
# To generate AND build from the command line:
999
# scons vsproj=yes vsproj_gen_only=no
1000
def generate_vs_project(env, original_args, project_name="godot"):
1001
# Augmented glob_recursive that also fills the dirs argument with traversed directories that have content.
1002
def glob_recursive_2(pattern, dirs, node="."):
1003
from SCons import Node
1004
from SCons.Script import Glob
1005
1006
results = []
1007
for f in Glob(str(node) + "/*", source=True):
1008
if type(f) is Node.FS.Dir:
1009
results += glob_recursive_2(pattern, dirs, f)
1010
r = Glob(str(node) + "/" + pattern, source=True)
1011
if len(r) > 0 and str(node) not in dirs:
1012
d = ""
1013
for part in str(node).split("\\"):
1014
d += part
1015
if d not in dirs:
1016
dirs.append(d)
1017
d += "\\"
1018
results += r
1019
return results
1020
1021
def get_bool(args, option, default):
1022
from SCons.Variables.BoolVariable import _text2bool
1023
1024
val = args.get(option, default)
1025
if val is not None:
1026
try:
1027
return _text2bool(val)
1028
except (ValueError, AttributeError):
1029
return default
1030
else:
1031
return default
1032
1033
def format_key_value(v):
1034
if type(v) in [tuple, list]:
1035
return v[0] if len(v) == 1 else f"{v[0]}={v[1]}"
1036
return v
1037
1038
def get_dependencies(file, env, exts, headers, sources, others):
1039
for child in file.children():
1040
if isinstance(child, str):
1041
child = env.File(x)
1042
fname = ""
1043
try:
1044
fname = child.path
1045
except AttributeError:
1046
# It's not a file.
1047
pass
1048
1049
if fname:
1050
parts = os.path.splitext(fname)
1051
if len(parts) > 1:
1052
ext = parts[1].lower()
1053
if ext in exts["sources"]:
1054
sources += [fname]
1055
elif ext in exts["headers"]:
1056
headers += [fname]
1057
elif ext in exts["others"]:
1058
others += [fname]
1059
1060
get_dependencies(child, env, exts, headers, sources, others)
1061
1062
filtered_args = original_args.copy()
1063
1064
# Ignore the "vsproj" option to not regenerate the VS project on every build
1065
filtered_args.pop("vsproj", None)
1066
1067
# This flag allows users to regenerate the proj files but skip the building process.
1068
# This lets projects be regenerated even if there are build errors.
1069
filtered_args.pop("vsproj_gen_only", None)
1070
1071
# This flag allows users to regenerate only the props file without touching the sln or vcxproj files.
1072
# This preserves any customizations users have done to the solution, while still updating the file list
1073
# and build commands.
1074
filtered_args.pop("vsproj_props_only", None)
1075
1076
# The "progress" option is ignored as the current compilation progress indication doesn't work in VS
1077
filtered_args.pop("progress", None)
1078
1079
# We add these three manually because they might not be explicitly passed in, and it's important to always set them.
1080
filtered_args.pop("platform", None)
1081
filtered_args.pop("target", None)
1082
filtered_args.pop("arch", None)
1083
1084
platform = env["platform"]
1085
target = env["target"]
1086
arch = env["arch"]
1087
host_arch = detect_arch()
1088
1089
host_platform = "windows"
1090
if (
1091
sys.platform.startswith("linux")
1092
or sys.platform.startswith("dragonfly")
1093
or sys.platform.startswith("freebsd")
1094
or sys.platform.startswith("netbsd")
1095
or sys.platform.startswith("openbsd")
1096
):
1097
host_platform = "linuxbsd"
1098
elif sys.platform == "darwin":
1099
host_platform = "macos"
1100
1101
vs_configuration = {}
1102
host_vs_configuration = {}
1103
common_build_prefix = []
1104
confs = []
1105
for x in sorted(glob.glob("platform/*")):
1106
# Only platforms that opt in to vs proj generation are included.
1107
if not os.path.isdir(x) or not os.path.exists(x + "/msvs.py"):
1108
continue
1109
tmppath = "./" + x
1110
sys.path.insert(0, tmppath)
1111
import msvs
1112
1113
vs_plats = []
1114
vs_confs = []
1115
try:
1116
platform_name = x[9:]
1117
vs_plats = msvs.get_platforms()
1118
vs_confs = msvs.get_configurations()
1119
val = []
1120
for plat in vs_plats:
1121
val += [{"platform": plat[0], "architecture": plat[1]}]
1122
1123
vsconf = {"platform": platform_name, "targets": vs_confs, "arches": val}
1124
confs += [vsconf]
1125
1126
# Save additional information about the configuration for the actively selected platform,
1127
# so we can generate the platform-specific props file with all the build commands/defines/etc
1128
if platform == platform_name:
1129
common_build_prefix = msvs.get_build_prefix(env)
1130
vs_configuration = vsconf
1131
if platform_name == host_platform:
1132
host_vs_configuration = vsconf
1133
for a in vsconf["arches"]:
1134
if host_arch == a["architecture"]:
1135
host_arch = a["platform"]
1136
break
1137
except Exception:
1138
pass
1139
1140
sys.path.remove(tmppath)
1141
sys.modules.pop("msvs")
1142
1143
extensions = {}
1144
extensions["headers"] = [".h", ".hh", ".hpp", ".hxx", ".inc"]
1145
extensions["sources"] = [".c", ".cc", ".cpp", ".cxx", ".m", ".mm", ".java"]
1146
extensions["others"] = [".natvis", ".glsl", ".rc"]
1147
1148
headers = []
1149
headers_dirs = []
1150
for ext in extensions["headers"]:
1151
for file in glob_recursive_2("*" + ext, headers_dirs):
1152
headers.append(str(file).replace("/", "\\"))
1153
1154
sources = []
1155
sources_dirs = []
1156
for ext in extensions["sources"]:
1157
for file in glob_recursive_2("*" + ext, sources_dirs):
1158
sources.append(str(file).replace("/", "\\"))
1159
1160
others = []
1161
others_dirs = []
1162
for ext in extensions["others"]:
1163
for file in glob_recursive_2("*" + ext, others_dirs):
1164
others.append(str(file).replace("/", "\\"))
1165
1166
skip_filters = False
1167
import hashlib
1168
import json
1169
1170
md5 = hashlib.md5(
1171
json.dumps(sorted(headers + headers_dirs + sources + sources_dirs + others + others_dirs)).encode("utf-8")
1172
).hexdigest()
1173
1174
if os.path.exists(f"{project_name}.vcxproj.filters"):
1175
with open(f"{project_name}.vcxproj.filters", "r", encoding="utf-8") as file:
1176
existing_filters = file.read()
1177
match = re.search(r"(?ms)^<!-- CHECKSUM$.([0-9a-f]{32})", existing_filters)
1178
if match is not None and md5 == match.group(1):
1179
skip_filters = True
1180
1181
import uuid
1182
1183
# Don't regenerate the filters file if nothing has changed, so we keep the existing UUIDs.
1184
if not skip_filters:
1185
print(f"Regenerating {project_name}.vcxproj.filters")
1186
1187
with open("misc/msvs/vcxproj.filters.template", "r", encoding="utf-8") as file:
1188
filters_template = file.read()
1189
for i in range(1, 10):
1190
filters_template = filters_template.replace(f"%%UUID{i}%%", str(uuid.uuid4()))
1191
1192
filters = ""
1193
1194
for d in headers_dirs:
1195
filters += f'<Filter Include="Header Files\\{d}"><UniqueIdentifier>{{{str(uuid.uuid4())}}}</UniqueIdentifier></Filter>\n'
1196
for d in sources_dirs:
1197
filters += f'<Filter Include="Source Files\\{d}"><UniqueIdentifier>{{{str(uuid.uuid4())}}}</UniqueIdentifier></Filter>\n'
1198
for d in others_dirs:
1199
filters += f'<Filter Include="Other Files\\{d}"><UniqueIdentifier>{{{str(uuid.uuid4())}}}</UniqueIdentifier></Filter>\n'
1200
1201
filters_template = filters_template.replace("%%FILTERS%%", filters)
1202
1203
filters = ""
1204
for file in headers:
1205
filters += (
1206
f'<ClInclude Include="{file}"><Filter>Header Files\\{os.path.dirname(file)}</Filter></ClInclude>\n'
1207
)
1208
filters_template = filters_template.replace("%%INCLUDES%%", filters)
1209
1210
filters = ""
1211
for file in sources:
1212
filters += (
1213
f'<ClCompile Include="{file}"><Filter>Source Files\\{os.path.dirname(file)}</Filter></ClCompile>\n'
1214
)
1215
1216
filters_template = filters_template.replace("%%COMPILES%%", filters)
1217
1218
filters = ""
1219
for file in others:
1220
filters += f'<None Include="{file}"><Filter>Other Files\\{os.path.dirname(file)}</Filter></None>\n'
1221
filters_template = filters_template.replace("%%OTHERS%%", filters)
1222
1223
filters_template = filters_template.replace("%%HASH%%", md5)
1224
1225
with open(f"{project_name}.vcxproj.filters", "w", encoding="utf-8", newline="\r\n") as f:
1226
f.write(filters_template)
1227
1228
headers_active = []
1229
sources_active = []
1230
others_active = []
1231
1232
get_dependencies(
1233
env.File(f"#bin/godot{env['PROGSUFFIX']}"), env, extensions, headers_active, sources_active, others_active
1234
)
1235
1236
all_items = []
1237
properties = []
1238
activeItems = []
1239
extraItems = []
1240
1241
set_headers = set(headers_active)
1242
set_sources = set(sources_active)
1243
set_others = set(others_active)
1244
for file in headers:
1245
base_path = os.path.dirname(file).replace("\\", "_")
1246
all_items.append(f'<ClInclude Include="{file}">')
1247
all_items.append(
1248
f" <ExcludedFromBuild Condition=\"!$(ActiveProjectItemList_{base_path}.Contains(';{file};'))\">true</ExcludedFromBuild>"
1249
)
1250
all_items.append("</ClInclude>")
1251
if file in set_headers:
1252
activeItems.append(file)
1253
1254
for file in sources:
1255
base_path = os.path.dirname(file).replace("\\", "_")
1256
all_items.append(f'<ClCompile Include="{file}">')
1257
all_items.append(
1258
f" <ExcludedFromBuild Condition=\"!$(ActiveProjectItemList_{base_path}.Contains(';{file};'))\">true</ExcludedFromBuild>"
1259
)
1260
all_items.append("</ClCompile>")
1261
if file in set_sources:
1262
activeItems.append(file)
1263
1264
for file in others:
1265
base_path = os.path.dirname(file).replace("\\", "_")
1266
all_items.append(f'<None Include="{file}">')
1267
all_items.append(
1268
f" <ExcludedFromBuild Condition=\"!$(ActiveProjectItemList_{base_path}.Contains(';{file};'))\">true</ExcludedFromBuild>"
1269
)
1270
all_items.append("</None>")
1271
if file in set_others:
1272
activeItems.append(file)
1273
1274
if vs_configuration:
1275
vsconf = ""
1276
for a in vs_configuration["arches"]:
1277
if arch == a["architecture"]:
1278
vsconf = f"{target}|{a['platform']}"
1279
break
1280
1281
condition = "'$(GodotConfiguration)|$(GodotPlatform)'=='" + vsconf + "'"
1282
itemlist = {}
1283
for item in activeItems:
1284
key = os.path.dirname(item).replace("\\", "_")
1285
if key not in itemlist:
1286
itemlist[key] = [item]
1287
else:
1288
itemlist[key] += [item]
1289
1290
for x in itemlist.keys():
1291
properties.append(
1292
"<ActiveProjectItemList_%s>;%s;</ActiveProjectItemList_%s>" % (x, ";".join(itemlist[x]), x)
1293
)
1294
output = os.path.join("bin", f"godot{env['PROGSUFFIX']}")
1295
1296
# The modules_enabled.gen.h header containing the defines is only generated on build, and only for the most recently built
1297
# platform, which means VS can't properly render code that's inside module-specific ifdefs. This adds those defines to the
1298
# platform-specific VS props file, so that VS knows which defines are enabled for the selected platform.
1299
env.Append(VSHINT_DEFINES=[f"MODULE_{module.upper()}_ENABLED" for module in env.module_list])
1300
1301
with open("misc/msvs/props.template", "r", encoding="utf-8") as file:
1302
props_template = file.read()
1303
1304
props_template = props_template.replace("%%CONDITION%%", condition)
1305
props_template = props_template.replace("%%PROPERTIES%%", "\n ".join(properties))
1306
props_template = props_template.replace("%%EXTRA_ITEMS%%", "\n ".join(extraItems))
1307
1308
props_template = props_template.replace("%%OUTPUT%%", output)
1309
1310
proplist = [format_key_value(j) for j in list(env["CPPDEFINES"])]
1311
proplist += [format_key_value(j) for j in env.get("VSHINT_DEFINES", [])]
1312
props_template = props_template.replace("%%DEFINES%%", ";".join(proplist))
1313
1314
proplist = [str(j) for j in env["CPPPATH"]]
1315
proplist += [str(j) for j in env.get("VSHINT_INCLUDES", [])]
1316
proplist += [str(j) for j in get_default_include_paths(env)]
1317
props_template = props_template.replace("%%INCLUDES%%", ";".join(proplist))
1318
1319
proplist = [env.subst("$CCFLAGS")]
1320
proplist += [env.subst("$CXXFLAGS")]
1321
proplist += [env.subst("$VSHINT_OPTIONS")]
1322
props_template = props_template.replace("%%OPTIONS%%", " ".join(proplist))
1323
1324
# Windows allows us to have spaces in paths, so we need
1325
# to double quote off the directory. However, the path ends
1326
# in a backslash, so we need to remove this, lest it escape the
1327
# last double quote off, confusing MSBuild
1328
common_build_postfix = [
1329
"--directory=&quot;$(ProjectDir.TrimEnd(&apos;\\&apos;))&quot;",
1330
"progress=no",
1331
f"platform={platform}",
1332
f"target={target}",
1333
f"arch={arch}",
1334
]
1335
1336
for arg, value in filtered_args.items():
1337
common_build_postfix.append(f"{arg}={value}")
1338
1339
cmd_rebuild = [
1340
"vsproj=yes",
1341
"vsproj_props_only=yes",
1342
"vsproj_gen_only=no",
1343
f"vsproj_name={project_name}",
1344
] + common_build_postfix
1345
1346
cmd_clean = [
1347
"--clean",
1348
] + common_build_postfix
1349
1350
commands = "scons"
1351
if len(common_build_prefix) == 0:
1352
commands = "echo Starting SCons &amp; " + commands
1353
else:
1354
common_build_prefix[0] = "echo Starting SCons &amp; " + common_build_prefix[0]
1355
1356
cmd = " ".join(common_build_prefix + [" ".join([commands] + common_build_postfix)])
1357
props_template = props_template.replace("%%BUILD%%", cmd)
1358
1359
cmd = " ".join(common_build_prefix + [" ".join([commands] + cmd_rebuild)])
1360
props_template = props_template.replace("%%REBUILD%%", cmd)
1361
1362
cmd = " ".join(common_build_prefix + [" ".join([commands] + cmd_clean)])
1363
props_template = props_template.replace("%%CLEAN%%", cmd)
1364
1365
with open(
1366
f"{project_name}.{platform}.{target}.{arch}.generated.props", "w", encoding="utf-8", newline="\r\n"
1367
) as f:
1368
f.write(props_template)
1369
1370
proj_uuid = str(uuid.uuid4())
1371
sln_uuid = str(uuid.uuid4())
1372
1373
if os.path.exists(f"{project_name}.sln"):
1374
for line in open(f"{project_name}.sln", "r", encoding="utf-8").read().splitlines():
1375
if line.startswith('Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}")'):
1376
proj_uuid = re.search(
1377
r"\"{(\b[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-\b[0-9a-fA-F]{12}\b)}\"$",
1378
line,
1379
).group(1)
1380
elif line.strip().startswith("SolutionGuid ="):
1381
sln_uuid = re.search(
1382
r"{(\b[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-\b[0-9a-fA-F]{12}\b)}", line
1383
).group(1)
1384
break
1385
1386
configurations = []
1387
imports = []
1388
properties = []
1389
section1 = []
1390
section2 = []
1391
for conf in confs:
1392
godot_platform = conf["platform"]
1393
has_editor = "editor" in conf["targets"]
1394
1395
# Skip any platforms that can build the editor and don't match the host platform.
1396
#
1397
# When both Windows and Mac define an editor target, it's defined as platform+target+arch (windows+editor+x64 for example).
1398
# VS only supports two attributes, a "Configuration" and a "Platform", and we currently map our target to the Configuration
1399
# (i.e. editor/template_debug/template_release), and our architecture to the "Platform" (i.e. x64, arm64, etc).
1400
# Those two are not enough to disambiguate multiple godot targets for different godot platforms with the same architecture,
1401
# i.e. editor|x64 would currently match both windows editor intel 64 and linux editor intel 64.
1402
#
1403
# TODO: More work is needed in order to support generating VS projects that unambiguously support all platform+target+arch variations.
1404
# The VS "Platform" has to be a known architecture that VS recognizes, so we can only play around with the "Configuration" part of the combo.
1405
if has_editor and godot_platform != host_vs_configuration["platform"]:
1406
continue
1407
1408
for p in conf["arches"]:
1409
sln_plat = p["platform"]
1410
proj_plat = sln_plat
1411
godot_arch = p["architecture"]
1412
1413
# Redirect editor configurations for platforms that don't support the editor target to the default editor target on the
1414
# active host platform, so the solution has all the permutations and VS doesn't complain about missing project configurations.
1415
# These configurations are disabled, so they show up but won't build.
1416
if not has_editor:
1417
section1 += [f"editor|{sln_plat} = editor|{proj_plat}"]
1418
section2 += [f"{{{proj_uuid}}}.editor|{proj_plat}.ActiveCfg = editor|{host_arch}"]
1419
1420
configurations += [
1421
f'<ProjectConfiguration Include="editor|{proj_plat}">',
1422
" <Configuration>editor</Configuration>",
1423
f" <Platform>{proj_plat}</Platform>",
1424
"</ProjectConfiguration>",
1425
]
1426
1427
properties += [
1428
f"<PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='editor|{proj_plat}'\">",
1429
" <GodotConfiguration>editor</GodotConfiguration>",
1430
f" <GodotPlatform>{proj_plat}</GodotPlatform>",
1431
"</PropertyGroup>",
1432
]
1433
1434
for t in conf["targets"]:
1435
godot_target = t
1436
1437
# Windows x86 is a special little flower that requires a project platform == Win32 but a solution platform == x86.
1438
if godot_platform == "windows" and godot_target == "editor" and godot_arch == "x86_32":
1439
sln_plat = "x86"
1440
1441
configurations += [
1442
f'<ProjectConfiguration Include="{godot_target}|{proj_plat}">',
1443
f" <Configuration>{godot_target}</Configuration>",
1444
f" <Platform>{proj_plat}</Platform>",
1445
"</ProjectConfiguration>",
1446
]
1447
1448
properties += [
1449
f"<PropertyGroup Condition=\"'$(Configuration)|$(Platform)'=='{godot_target}|{proj_plat}'\">",
1450
f" <GodotConfiguration>{godot_target}</GodotConfiguration>",
1451
f" <GodotPlatform>{proj_plat}</GodotPlatform>",
1452
"</PropertyGroup>",
1453
]
1454
1455
p = f"{project_name}.{godot_platform}.{godot_target}.{godot_arch}.generated.props"
1456
imports += [
1457
f'<Import Project="$(MSBuildProjectDirectory)\\{p}" Condition="Exists(\'$(MSBuildProjectDirectory)\\{p}\')"/>'
1458
]
1459
1460
section1 += [f"{godot_target}|{sln_plat} = {godot_target}|{sln_plat}"]
1461
1462
section2 += [
1463
f"{{{proj_uuid}}}.{godot_target}|{sln_plat}.ActiveCfg = {godot_target}|{proj_plat}",
1464
f"{{{proj_uuid}}}.{godot_target}|{sln_plat}.Build.0 = {godot_target}|{proj_plat}",
1465
]
1466
1467
# Add an extra import for a local user props file at the end, so users can add more overrides.
1468
imports += [
1469
f'<Import Project="$(MSBuildProjectDirectory)\\{project_name}.vs.user.props" Condition="Exists(\'$(MSBuildProjectDirectory)\\{project_name}.vs.user.props\')"/>'
1470
]
1471
section1 = sorted(section1)
1472
section2 = sorted(section2)
1473
1474
if not get_bool(original_args, "vsproj_props_only", False):
1475
with open("misc/msvs/vcxproj.template", "r", encoding="utf-8") as file:
1476
proj_template = file.read()
1477
proj_template = proj_template.replace("%%UUID%%", proj_uuid)
1478
proj_template = proj_template.replace("%%CONFS%%", "\n ".join(configurations))
1479
proj_template = proj_template.replace("%%IMPORTS%%", "\n ".join(imports))
1480
proj_template = proj_template.replace("%%DEFAULT_ITEMS%%", "\n ".join(all_items))
1481
proj_template = proj_template.replace("%%PROPERTIES%%", "\n ".join(properties))
1482
1483
with open(f"{project_name}.vcxproj", "w", encoding="utf-8", newline="\r\n") as f:
1484
f.write(proj_template)
1485
1486
if not get_bool(original_args, "vsproj_props_only", False):
1487
with open("misc/msvs/sln.template", "r", encoding="utf-8") as file:
1488
sln_template = file.read()
1489
sln_template = sln_template.replace("%%NAME%%", project_name)
1490
sln_template = sln_template.replace("%%UUID%%", proj_uuid)
1491
sln_template = sln_template.replace("%%SLNUUID%%", sln_uuid)
1492
sln_template = sln_template.replace("%%SECTION1%%", "\n\t\t".join(section1))
1493
sln_template = sln_template.replace("%%SECTION2%%", "\n\t\t".join(section2))
1494
1495
with open(f"{project_name}.sln", "w", encoding="utf-8", newline="\r\n") as f:
1496
f.write(sln_template)
1497
1498
if get_bool(original_args, "vsproj_gen_only", True):
1499
sys.exit()
1500
1501
1502
############################################################
1503
# FILE GENERATION & FORMATTING
1504
############################################################
1505
1506
1507
def generate_copyright_header(filename: str) -> str:
1508
MARGIN = 70
1509
TEMPLATE = """\
1510
/**************************************************************************/
1511
/* %s*/
1512
/**************************************************************************/
1513
/* This file is part of: */
1514
/* GODOT ENGINE */
1515
/* https://godotengine.org */
1516
/**************************************************************************/
1517
/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
1518
/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
1519
/* */
1520
/* Permission is hereby granted, free of charge, to any person obtaining */
1521
/* a copy of this software and associated documentation files (the */
1522
/* "Software"), to deal in the Software without restriction, including */
1523
/* without limitation the rights to use, copy, modify, merge, publish, */
1524
/* distribute, sublicense, and/or sell copies of the Software, and to */
1525
/* permit persons to whom the Software is furnished to do so, subject to */
1526
/* the following conditions: */
1527
/* */
1528
/* The above copyright notice and this permission notice shall be */
1529
/* included in all copies or substantial portions of the Software. */
1530
/* */
1531
/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
1532
/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
1533
/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */
1534
/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
1535
/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
1536
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
1537
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
1538
/**************************************************************************/
1539
"""
1540
if len(filename := os.path.basename(filename).ljust(MARGIN)) > MARGIN:
1541
print_warning(f'Filename "{filename}" too large for copyright header.')
1542
return TEMPLATE % filename
1543
1544
1545
@contextlib.contextmanager
1546
def generated_wrapper(
1547
path: str,
1548
guard: Optional[bool] = None,
1549
) -> Generator[TextIOBase, None, None]:
1550
"""
1551
Wrapper class to automatically handle copyright headers and header guards
1552
for generated scripts. Meant to be invoked via `with` statement similar to
1553
creating a file.
1554
1555
- `path`: The path of the file to be created.
1556
- `guard`: Optional bool to determine if `#pragma once` should be added. If
1557
unassigned, the value is determined by file extension.
1558
"""
1559
1560
with open(path, "wt", encoding="utf-8", newline="\n") as file:
1561
if not path.endswith(".out"): # For test output, we only care about the content.
1562
file.write(generate_copyright_header(path))
1563
file.write("\n/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n")
1564
1565
if guard is None:
1566
guard = path.endswith((".h", ".hh", ".hpp", ".hxx", ".inc"))
1567
if guard:
1568
file.write("#pragma once\n\n")
1569
1570
with StringIO(newline="\n") as str_io:
1571
yield str_io
1572
file.write(str_io.getvalue().strip() or "/* NO CONTENT */")
1573
1574
file.write("\n")
1575
1576
1577
def get_buffer(path: str) -> bytes:
1578
with open(path, "rb") as file:
1579
return file.read()
1580
1581
1582
def compress_buffer(buffer: bytes) -> bytes:
1583
# Use maximum zlib compression level to further reduce file size
1584
# (at the cost of initial build times).
1585
return zlib.compress(buffer, zlib.Z_BEST_COMPRESSION)
1586
1587
1588
def format_buffer(buffer: bytes, indent: int = 0, width: int = 120, initial_indent: bool = False) -> str:
1589
return textwrap.fill(
1590
", ".join(str(byte) for byte in buffer),
1591
width=width,
1592
initial_indent="\t" * indent if initial_indent else "",
1593
subsequent_indent="\t" * indent,
1594
tabsize=4,
1595
)
1596
1597
1598
############################################################
1599
# CSTRING PARSING
1600
############################################################
1601
1602
C_ESCAPABLES = [
1603
("\\", "\\\\"),
1604
("\a", "\\a"),
1605
("\b", "\\b"),
1606
("\f", "\\f"),
1607
("\n", "\\n"),
1608
("\r", "\\r"),
1609
("\t", "\\t"),
1610
("\v", "\\v"),
1611
# ("'", "\\'"), # Skip, as we're only dealing with full strings.
1612
('"', '\\"'),
1613
]
1614
C_ESCAPE_TABLE = str.maketrans(dict((x, y) for x, y in C_ESCAPABLES))
1615
1616
1617
def to_escaped_cstring(value: str) -> str:
1618
return value.translate(C_ESCAPE_TABLE)
1619
1620
1621
def to_raw_cstring(value: Union[str, List[str]]) -> str:
1622
MAX_LITERAL = 16 * 1024
1623
1624
if isinstance(value, list):
1625
value = "\n".join(value) + "\n"
1626
1627
split: List[bytes] = []
1628
offset = 0
1629
encoded = value.encode()
1630
1631
while offset <= len(encoded):
1632
segment = encoded[offset : offset + MAX_LITERAL]
1633
offset += MAX_LITERAL
1634
if len(segment) == MAX_LITERAL:
1635
# Try to segment raw strings at double newlines to keep readable.
1636
pretty_break = segment.rfind(b"\n\n")
1637
if pretty_break != -1:
1638
segment = segment[: pretty_break + 1]
1639
offset -= MAX_LITERAL - pretty_break - 1
1640
# If none found, ensure we end with valid utf8.
1641
# https://github.com/halloleo/unicut/blob/master/truncate.py
1642
elif segment[-1] & 0b10000000:
1643
last_11xxxxxx_index = [i for i in range(-1, -5, -1) if segment[i] & 0b11000000 == 0b11000000][0]
1644
last_11xxxxxx = segment[last_11xxxxxx_index]
1645
if not last_11xxxxxx & 0b00100000:
1646
last_char_length = 2
1647
elif not last_11xxxxxx & 0b0010000:
1648
last_char_length = 3
1649
elif not last_11xxxxxx & 0b0001000:
1650
last_char_length = 4
1651
1652
if last_char_length > -last_11xxxxxx_index:
1653
segment = segment[:last_11xxxxxx_index]
1654
offset += last_11xxxxxx_index
1655
1656
split += [segment]
1657
1658
if len(split) == 1:
1659
return f'R"<!>({split[0].decode()})<!>"'
1660
else:
1661
# Wrap multiple segments in parenthesis to suppress `string-concatenation` warnings on clang.
1662
return "({})".format(" ".join(f'R"<!>({segment.decode()})<!>"' for segment in split))
1663
1664
1665
def get_default_include_paths(env):
1666
if env.msvc:
1667
return []
1668
compiler = env.subst("$CXX")
1669
target = os.path.join(env.Dir("#main").abspath, "main.cpp")
1670
args = [compiler, target, "-x", "c++", "-v"]
1671
ret = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
1672
output = ret.stdout
1673
match = re.search(r"#include <\.\.\.> search starts here:([\S\s]*)End of search list.", output)
1674
if not match:
1675
print_warning("Failed to find the include paths in the compiler output.")
1676
return []
1677
return [x.strip() for x in match[1].strip().splitlines()]
1678
1679