Skip to content

Commit fb9eb33

Browse files
committed
Add scu_build option
1 parent 24d79ab commit fb9eb33

File tree

2 files changed

+330
-7
lines changed

2 files changed

+330
-7
lines changed

scu_builders.py

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
"""Functions used to generate scu build source files during build time"""
2+
3+
import glob
4+
import math
5+
import os
6+
from pathlib import Path
7+
8+
base_folder_path = str(Path(__file__).parent) + "/"
9+
base_folder_only = os.path.basename(os.path.normpath(base_folder_path))
10+
_verbose = False # Set manually for debug prints
11+
_scu_folders = set()
12+
_max_includes_per_scu = 1024
13+
14+
15+
def clear_out_stale_files(output_folder, extension, fresh_files):
16+
output_folder = os.path.abspath(output_folder)
17+
# print("clear_out_stale_files from folder: " + output_folder)
18+
19+
if not os.path.isdir(output_folder):
20+
# folder does not exist or has not been created yet,
21+
# no files to clearout. (this is not an error)
22+
return
23+
24+
for file in glob.glob(output_folder + "/*." + extension):
25+
file = Path(file)
26+
if file not in fresh_files:
27+
# print("removed stale file: " + str(file))
28+
os.remove(file)
29+
30+
31+
def folder_not_found(folder):
32+
abs_folder = base_folder_path + folder + "/"
33+
return not os.path.isdir(abs_folder)
34+
35+
36+
def find_files_in_folder(folder, sub_folder, include_list, extension, sought_exceptions, found_exceptions):
37+
abs_folder = base_folder_path + folder + "/" + sub_folder
38+
39+
if not os.path.isdir(abs_folder):
40+
print(f'SCU: "{abs_folder}" not found.')
41+
return include_list, found_exceptions
42+
43+
os.chdir(abs_folder)
44+
45+
sub_folder_slashed = ""
46+
if sub_folder != "":
47+
sub_folder_slashed = sub_folder + "/"
48+
49+
for file in glob.glob("*." + extension):
50+
simple_name = Path(file).stem
51+
52+
if file.endswith(".gen.cpp"):
53+
continue
54+
55+
li = '#include "' + folder + "/" + sub_folder_slashed + file + '"'
56+
57+
if simple_name not in sought_exceptions:
58+
include_list.append(li)
59+
else:
60+
found_exceptions.append(li)
61+
62+
return include_list, found_exceptions
63+
64+
65+
def write_output_file(file_count, include_list, start_line, end_line, output_folder, output_filename_prefix, extension):
66+
output_folder = os.path.abspath(output_folder)
67+
68+
if not os.path.isdir(output_folder):
69+
# create
70+
os.mkdir(output_folder)
71+
if not os.path.isdir(output_folder):
72+
print(f'SCU: "{output_folder}" could not be created.')
73+
return
74+
if _verbose:
75+
print("SCU: Creating folder: %s" % output_folder)
76+
77+
file_text = ""
78+
79+
for i in range(start_line, end_line):
80+
if i < len(include_list):
81+
line = include_list[i]
82+
li = line + "\n"
83+
file_text += li
84+
85+
num_string = ""
86+
if file_count > 0:
87+
num_string = "_" + str(file_count)
88+
89+
short_filename = output_filename_prefix + num_string + ".gen." + extension
90+
output_filename = output_folder + "/" + short_filename
91+
output_path = Path(output_filename)
92+
93+
if not output_path.exists() or output_path.read_text() != file_text:
94+
if _verbose:
95+
print("SCU: Generating: %s" % short_filename)
96+
output_path.write_text(file_text, encoding="utf8")
97+
elif _verbose:
98+
print("SCU: Generation not needed for: " + short_filename)
99+
100+
return output_path
101+
102+
103+
def write_exception_output_file(file_count, exception_string, output_folder, output_filename_prefix, extension):
104+
output_folder = os.path.abspath(output_folder)
105+
if not os.path.isdir(output_folder):
106+
print(f"SCU: {output_folder} does not exist.")
107+
return
108+
109+
file_text = exception_string + "\n"
110+
111+
num_string = ""
112+
if file_count > 0:
113+
num_string = "_" + str(file_count)
114+
115+
short_filename = output_filename_prefix + "_exception" + num_string + ".gen." + extension
116+
output_filename = output_folder + "/" + short_filename
117+
118+
output_path = Path(output_filename)
119+
120+
if not output_path.exists() or output_path.read_text() != file_text:
121+
if _verbose:
122+
print("SCU: Generating: " + short_filename)
123+
output_path.write_text(file_text, encoding="utf8")
124+
elif _verbose:
125+
print("SCU: Generation not needed for: " + short_filename)
126+
127+
return output_path
128+
129+
130+
def find_section_name(sub_folder):
131+
# Construct a useful name for the section from the path for debug logging
132+
section_path = os.path.abspath(base_folder_path + sub_folder) + "/"
133+
134+
folders = []
135+
folder = ""
136+
137+
for i in range(8):
138+
folder = os.path.dirname(section_path)
139+
folder = os.path.basename(folder)
140+
if folder == base_folder_only:
141+
break
142+
folders.append(folder)
143+
section_path += "../"
144+
section_path = os.path.abspath(section_path) + "/"
145+
146+
section_name = ""
147+
for n in range(len(folders)):
148+
section_name += folders[len(folders) - n - 1]
149+
if n != (len(folders) - 1):
150+
section_name += "_"
151+
152+
return section_name
153+
154+
155+
# "folders" is a list of folders to add all the files from to add to the SCU
156+
# "section (like a module)". The name of the scu file will be derived from the first folder
157+
# (thus e.g. scene/3d becomes scu_scene_3d.gen.cpp)
158+
159+
# "includes_per_scu" limits the number of includes in a single scu file.
160+
# This allows the module to be built in several translation units instead of just 1.
161+
# This will usually be slower to compile but will use less memory per compiler instance, which
162+
# is most relevant in release builds.
163+
164+
# "sought_exceptions" are a list of files (without extension) that contain
165+
# e.g. naming conflicts, and are therefore not suitable for the scu build.
166+
# These will automatically be placed in their own separate scu file,
167+
# which is slow like a normal build, but prevents the naming conflicts.
168+
# Ideally in these situations, the source code should be changed to prevent naming conflicts.
169+
170+
171+
# "extension" will usually be cpp, but can also be set to c (for e.g. third party libraries that use c)
172+
def process_folder(folders, sought_exceptions=[], includes_per_scu=0, extension="cpp"):
173+
if len(folders) == 0:
174+
return
175+
176+
# Construct the filename prefix from the FIRST folder name
177+
# e.g. "scene_3d"
178+
out_filename = find_section_name(folders[0])
179+
180+
found_includes = []
181+
found_exceptions = []
182+
183+
main_folder = folders[0]
184+
abs_main_folder = base_folder_path + main_folder
185+
186+
# Keep a record of all folders that have been processed for SCU,
187+
# this enables deciding what to do when we call "add_source_files()"
188+
global _scu_folders
189+
_scu_folders.add(main_folder)
190+
191+
# main folder (first)
192+
found_includes, found_exceptions = find_files_in_folder(
193+
main_folder, "", found_includes, extension, sought_exceptions, found_exceptions
194+
)
195+
196+
# sub folders
197+
for d in range(1, len(folders)):
198+
found_includes, found_exceptions = find_files_in_folder(
199+
main_folder, folders[d], found_includes, extension, sought_exceptions, found_exceptions
200+
)
201+
202+
found_includes = sorted(found_includes)
203+
204+
# calculate how many lines to write in each file
205+
total_lines = len(found_includes)
206+
207+
# adjust number of output files according to whether DEV or release
208+
num_output_files = 1
209+
210+
if includes_per_scu == 0:
211+
includes_per_scu = _max_includes_per_scu
212+
else:
213+
if includes_per_scu > _max_includes_per_scu:
214+
includes_per_scu = _max_includes_per_scu
215+
216+
num_output_files = max(math.ceil(total_lines / float(includes_per_scu)), 1)
217+
218+
lines_per_file = math.ceil(total_lines / float(num_output_files))
219+
lines_per_file = max(lines_per_file, 1)
220+
221+
start_line = 0
222+
223+
# These do not vary throughout the loop
224+
output_folder = abs_main_folder + "/.scu/"
225+
output_filename_prefix = "scu_" + out_filename
226+
227+
fresh_files = set()
228+
229+
for file_count in range(0, num_output_files):
230+
end_line = start_line + lines_per_file
231+
232+
# special case to cover rounding error in final file
233+
if file_count == (num_output_files - 1):
234+
end_line = len(found_includes)
235+
236+
fresh_file = write_output_file(
237+
file_count, found_includes, start_line, end_line, output_folder, output_filename_prefix, extension
238+
)
239+
240+
fresh_files.add(fresh_file)
241+
242+
start_line = end_line
243+
244+
# Write the exceptions each in their own scu gen file,
245+
# so they can effectively compile in "old style / normal build".
246+
for exception_count in range(len(found_exceptions)):
247+
fresh_file = write_exception_output_file(
248+
exception_count, found_exceptions[exception_count], output_folder, output_filename_prefix, extension
249+
)
250+
251+
fresh_files.add(fresh_file)
252+
253+
# Clear out any stale file (usually we will be overwriting if necessary,
254+
# but we want to remove any that are pre-existing that will not be
255+
# overwritten, so as to not compile anything stale).
256+
clear_out_stale_files(output_folder, extension, fresh_files)
257+
258+
259+
def generate_scu_files(max_includes_per_scu):
260+
global _max_includes_per_scu
261+
_max_includes_per_scu = max_includes_per_scu
262+
263+
print("SCU: Generating build files... (max includes per SCU: %d)" % _max_includes_per_scu)
264+
265+
curr_folder = os.path.abspath("./")
266+
267+
# check we are running from the correct folder
268+
if folder_not_found("src") or folder_not_found("gen"):
269+
raise RuntimeError("scu_builders.py must be run from the godot-cpp folder.")
270+
return
271+
272+
process_folder(["src"])
273+
process_folder(["src/classes"])
274+
process_folder(["src/core"])
275+
process_folder(["src/variant"])
276+
277+
process_folder(["gen/src/classes"])
278+
process_folder(["gen/src/variant"])
279+
280+
# Finally change back the path to the calling folder
281+
os.chdir(curr_folder)
282+
283+
if _verbose:
284+
print("SCU: Processed folders: %s" % sorted(_scu_folders))
285+
286+
return _scu_folders

tools/godotcpp.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from SCons.Variables import BoolVariable, EnumVariable, PathVariable
1212
from SCons.Variables.BoolVariable import _text2bool
1313

14+
import scu_builders
1415
from binding_generator import _generate_bindings, _get_file_list, get_file_list
1516
from build_profile import generate_trimmed_api
1617
from doc_source_generator import scons_generate_doc_source
@@ -137,6 +138,19 @@ def scons_emit_files(target, source, env):
137138
if profile_filepath:
138139
profile_filepath = normalize_path(profile_filepath, env)
139140

141+
# Clean scu files
142+
if not env["gen_scu_build"] and not env["scu_build"]:
143+
for root, dirs, files in os.walk(".", topdown=False):
144+
if os.path.basename(root) == ".scu":
145+
for name in files:
146+
os.remove(os.path.join(root, name))
147+
for name in dirs:
148+
os.rmdir(os.path.join(root, name))
149+
os.rmdir(root)
150+
151+
if ".scu" in dirs:
152+
dirs.remove(".scu")
153+
140154
# Always clean all files
141155
env.Clean(target, [env.File(f) for f in get_file_list(str(source[0]), target[0].abspath, True, True)])
142156

@@ -377,6 +391,9 @@ def options(opts, env):
377391
opts.Add(BoolVariable("debug_symbols", "Build with debugging symbols", True))
378392
opts.Add(BoolVariable("dev_build", "Developer build with dev-only debugging code (DEV_ENABLED)", False))
379393
opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", False))
394+
opts.Add(BoolVariable("scu_build", "Use single compilation unit build", False))
395+
opts.Add(BoolVariable("gen_scu_build", "Generate scu files", False))
396+
opts.Add("scu_limit", "Max includes per SCU file when using scu_build (determines RAM use)", "0")
380397

381398
# Add platform options (custom tools can override platforms)
382399
for pl in sorted(set(platforms + custom_platforms)):
@@ -553,14 +570,34 @@ def _godot_cpp(env):
553570
env.AlwaysBuild(bindings)
554571
env.NoCache(bindings)
555572

573+
# Run SCU file generation script if in a SCU build.
574+
if env["gen_scu_build"]:
575+
env.AppendUnique(CPPPATH=".")
576+
max_includes_per_scu = 8
577+
if env.dev_build:
578+
max_includes_per_scu = 1024
579+
580+
read_scu_limit = int(env["scu_limit"])
581+
read_scu_limit = max(0, min(read_scu_limit, 1024))
582+
if read_scu_limit != 0:
583+
max_includes_per_scu = read_scu_limit
584+
585+
scu_builders.generate_scu_files(max_includes_per_scu)
586+
556587
# Sources to compile
557-
sources = [
558-
*env.Glob("src/*.cpp"),
559-
*env.Glob("src/classes/*.cpp"),
560-
*env.Glob("src/core/*.cpp"),
561-
*env.Glob("src/variant/*.cpp"),
562-
*tuple(f for f in bindings if str(f).endswith(".cpp")),
563-
]
588+
if env["scu_build"]:
589+
sources = []
590+
for f in {"src/classes", "src/variant", "gen/src/variant", "src/core", "gen/src/classes", "src"}:
591+
sources.append(env.Glob(f.replace("\\", "/") + "/.scu/*.cpp"))
592+
env.AppendUnique(CPPPATH=".")
593+
else:
594+
sources = [
595+
*env.Glob("src/*.cpp"),
596+
*env.Glob("src/classes/*.cpp"),
597+
*env.Glob("src/core/*.cpp"),
598+
*env.Glob("src/variant/*.cpp"),
599+
*tuple(f for f in bindings if str(f).endswith(".cpp")),
600+
]
564601

565602
# Includes
566603
env.AppendUnique(

0 commit comments

Comments
 (0)