commit c2a409ebb13220928c6fd78d2d31eabaf73cc15a Author: Patrick Wuttke Date: Thu Dec 15 15:18:35 2022 +0100 Initial commit diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ff9e935 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,13 @@ + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document, and changing it is allowed as long +as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f0e7736 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# 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. +``` diff --git a/unity_build.py b/unity_build.py new file mode 100644 index 0000000..c9969bb --- /dev/null +++ b/unity_build.py @@ -0,0 +1,117 @@ + +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')