Skip to content

Commit 64eac33

Browse files
committed
Add scu_build option
1 parent 24d79ab commit 64eac33

File tree

2 files changed

+334
-7
lines changed

2 files changed

+334
-7
lines changed

scu_builders.py

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
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+
226+
# Clear old dir and files
227+
if os.path.exists(output_folder):
228+
for root, dirs, files in os.walk(output_folder, topdown=False):
229+
for name in files:
230+
os.remove(os.path.join(root, name))
231+
for name in dirs:
232+
os.rmdir(os.path.join(root, name))
233+
os.rmdir(output_folder)
234+
235+
output_filename_prefix = "scu_" + out_filename
236+
237+
fresh_files = set()
238+
239+
for file_count in range(0, num_output_files):
240+
end_line = start_line + lines_per_file
241+
242+
# special case to cover rounding error in final file
243+
if file_count == (num_output_files - 1):
244+
end_line = len(found_includes)
245+
246+
fresh_file = write_output_file(
247+
file_count, found_includes, start_line, end_line, output_folder, output_filename_prefix, extension
248+
)
249+
250+
fresh_files.add(fresh_file)
251+
252+
start_line = end_line
253+
254+
# Write the exceptions each in their own scu gen file,
255+
# so they can effectively compile in "old style / normal build".
256+
for exception_count in range(len(found_exceptions)):
257+
fresh_file = write_exception_output_file(
258+
exception_count, found_exceptions[exception_count], output_folder, output_filename_prefix, extension
259+
)
260+
261+
fresh_files.add(fresh_file)
262+
263+
# Clear out any stale file (usually we will be overwriting if necessary,
264+
# but we want to remove any that are pre-existing that will not be
265+
# overwritten, so as to not compile anything stale).
266+
clear_out_stale_files(output_folder, extension, fresh_files)
267+
268+
269+
def generate_scu_files(max_includes_per_scu):
270+
global _max_includes_per_scu
271+
_max_includes_per_scu = max_includes_per_scu
272+
273+
print("SCU: Generating build files... (max includes per SCU: %d)" % _max_includes_per_scu)
274+
275+
curr_folder = os.path.abspath("./")
276+
277+
# check we are running from the correct folder
278+
if folder_not_found("src") or folder_not_found("gen"):
279+
raise RuntimeError("scu_builders.py must be run from the godot-cpp folder.")
280+
return
281+
282+
process_folder(["src"])
283+
process_folder(["src/classes"])
284+
process_folder(["src/core"])
285+
process_folder(["src/variant"])
286+
287+
process_folder(["gen/src/classes"])
288+
process_folder(["gen/src/variant"])
289+
290+
# Finally change back the path to the calling folder
291+
os.chdir(curr_folder)
292+
293+
if _verbose:
294+
print("SCU: Processed folders: %s" % sorted(_scu_folders))
295+
296+
return _scu_folders

tools/godotcpp.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,20 @@
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
1718

19+
# Listing all the folders we have converted
20+
# for SCU in scu_builders.py
21+
_scu_folders = set()
22+
23+
24+
def set_scu_folders(scu_folders):
25+
global _scu_folders
26+
_scu_folders = scu_folders
27+
1828

1929
def get_cmdline_bool(option, default):
2030
"""We use `ARGUMENTS.get()` to check if options were manually overridden on the command line,
@@ -377,6 +387,8 @@ def options(opts, env):
377387
opts.Add(BoolVariable("debug_symbols", "Build with debugging symbols", True))
378388
opts.Add(BoolVariable("dev_build", "Developer build with dev-only debugging code (DEV_ENABLED)", False))
379389
opts.Add(BoolVariable("verbose", "Enable verbose output for the compilation", False))
390+
opts.Add(BoolVariable("scu_build", "Use single compilation unit build", False))
391+
opts.Add("scu_limit", "Max includes per SCU file when using scu_build (determines RAM use)", "0")
380392

381393
# Add platform options (custom tools can override platforms)
382394
for pl in sorted(set(platforms + custom_platforms)):
@@ -553,14 +565,33 @@ def _godot_cpp(env):
553565
env.AlwaysBuild(bindings)
554566
env.NoCache(bindings)
555567

568+
# Run SCU file generation script if in a SCU build.
569+
if env["scu_build"]:
570+
env.AppendUnique(CPPPATH=".")
571+
max_includes_per_scu = 8
572+
if env.dev_build:
573+
max_includes_per_scu = 1024
574+
575+
read_scu_limit = int(env["scu_limit"])
576+
read_scu_limit = max(0, min(read_scu_limit, 1024))
577+
if read_scu_limit != 0:
578+
max_includes_per_scu = read_scu_limit
579+
580+
set_scu_folders(scu_builders.generate_scu_files(max_includes_per_scu))
581+
556582
# 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-
]
583+
if env["scu_build"]:
584+
sources = []
585+
for f in _scu_folders:
586+
sources.append(env.Glob(f.replace("\\", "/") + "/.scu/*.cpp"))
587+
else:
588+
sources = [
589+
*env.Glob("src/*.cpp"),
590+
*env.Glob("src/classes/*.cpp"),
591+
*env.Glob("src/core/*.cpp"),
592+
*env.Glob("src/variant/*.cpp"),
593+
*tuple(f for f in bindings if str(f).endswith(".cpp")),
594+
]
564595

565596
# Includes
566597
env.AppendUnique(

0 commit comments

Comments
 (0)