scons_unity_build/unity_build.py
2022-12-15 15:19:37 +01:00

118 lines
4.5 KiB
Python

import os
import math
from SCons.Script import *
from SCons.Node.FS import File
from SCons import Action
"""
Scons Unity Build Generator
Provides several generators for SCons to combine multiple source files into a bigger
one to reduce compilation time, so called "unity builds". This is achieved by generating
unity source files which in term include the actual source files and compile them using
one of the existing SCons builders.
Usage
-----
In order to use this, just place it inside your `site_scons/site_tools` folder, enable it by
adding "unity_build" to the tools when constructing your Environment and replace invocations
of the Program/Library/SharedLibrary/StaticLibrary builders with their Unity... counterpart:
env = Environment(tools = ['default', 'unity_build'])
source_files = ...
env.UnityProgram(
target = 'my_program',
source = source_files,
...
)
The tool will generate an amount of unity source files and invoke the Program builder on these,
forwarding any other arguments you passed.
Other Options
------------
You can control the behaviour of the builder using several Environment options:
env['UNITY_CACHE_DIR'] = '.unity' # Directory where the unity sources are stored.
# can be either a string or a Dir() node.
env['UNITY_MAX_SOURCES'] = 15 # Maximum number of source files per unity file.
env['UNITY_MIN_FILES'] = env.GetOption('num_jobs')
# Minimum number of unity files to generate (if possible).
# Defaults to the number of jobs passed to SCons.
env['UNITY_DISABLE'] = False # Set to True to completely disable unity builds. The commands
# will simply pass through their options to the regular builders.
"""
def exists(env : Environment):
return True
def generate(env : Environment):
env.AddMethod(_make_generator(env.Program), 'UnityProgram')
env.AddMethod(_make_generator(env.Library), 'UnityLibrary')
env.AddMethod(_make_generator(env.StaticLibrary), 'UnityStaticLibrary')
env.AddMethod(_make_generator(env.SharedLibrary), 'UnitySharedLibrary')
env.SetDefault(UNITY_CACHE_DIR = '.unity')
env.SetDefault(UNITY_MAX_SOURCES = 15)
env.SetDefault(UNITY_MIN_FILES = env.GetOption('num_jobs'))
env.SetDefault(UNITY_DISABLE = False)
def _make_generator(base_generator):
def generator(env, source, target, cache_dir = None, *args, **kwargs):
if env['UNITY_DISABLE']:
return base_generator(target = target, source = source, *args, **kwargs)
unity_source_files = []
source_files, other_nodes = _flatten_source(source)
max_sources_per_file = max(1, math.ceil(len(source_files) / env['UNITY_MIN_FILES']))
sources_per_file = min(max_sources_per_file, env['UNITY_MAX_SOURCES'])
num_unity_files = math.ceil(len(source_files) / sources_per_file)
if not cache_dir:
cache_dir = env['UNITY_CACHE_DIR']
if not isinstance(cache_dir, str):
cache_dir = cache_dir.abspath
os.makedirs(cache_dir, exist_ok=True)
target_base_name = os.path.basename(target)
for idx in range(num_unity_files):
unity_filename = f'{cache_dir}/{target_base_name}_{idx}.cpp'
unity_source_files.append(unity_filename)
begin = sources_per_file*idx
end = sources_per_file*(idx+1)
_generate_unity_file(unity_filename, source_files[begin:end])
if len(other_nodes) > 0:
print(f'Exluded {len(other_nodes)} node(s) from Unity build.')
return [base_generator(target = target, source = unity_source_files + other_nodes, *args, **kwargs)]
return generator
def _flatten_source(source : list):
source_files = []
other_nodes = []
for ele in source:
if isinstance(ele, list):
more_sources, more_other = _flatten_source(ele)
source_files.extend(more_sources)
other_nodes.extend(more_other)
elif isinstance(ele, File):
source_files.append(ele.abspath)
elif isinstance(ele, str):
source_files.append(ele)
else:
print(type(ele))
other_nodes.append(ele)
return source_files, other_nodes
def _generate_unity_file(unity_filename, sources):
print(f'Generating {unity_filename} from {len(sources)} source files.')
with open(unity_filename, 'w') as f:
for source in sources:
fpath = os.path.abspath(source).replace("\\", "\\\\")
f.write(f'#include "{fpath}"\n')