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