diff --git a/SConstruct b/SConstruct index bbb2775..4f4d44c 100644 --- a/SConstruct +++ b/SConstruct @@ -6,59 +6,15 @@ # # Eryn Wells -import os - -import erw +import logging +setup_logging() BUILD_CMDS = get_bool_argument(ARGUMENTS.get('BUILD_CMDS', False)) MODE = ARGUMENTS.get('MODE', None) -BUILD_DIR = Dir('#build') -LIB_DIR = Dir('#lib') -SRC_DIR = Dir('#src') -TEST_DIR = Dir('#test') - - -modes = { - 'debug': erw.DebugEnvironment(succinct=not BUILD_CMDS), - 'beta': erw.BetaEnvironment(succinct=not BUILD_CMDS), - 'release': erw.ReleaseEnvironment(succinct=not BUILD_CMDS), -} - for mode in (MODE.split(',') if MODE else ['debug']): - try: - env = modes[mode] - except KeyError: - print 'Skipping invalid mode: {}'.format(mode) - break - - out_dir = BUILD_DIR.Dir(env['NAME']) - - # Allow same directory includes. - env.Append(CPPPATH=['.']) - - # Process all lib dirs. - env['AVAILABLE_LIBS'] = {} - for lib in os.listdir(LIB_DIR.abspath): - lib_dir = LIB_DIR.Dir(lib) - if not lib_dir.isdir(): - print 'Skipping {} in lib directory: is not a directory'.format(lib) - continue - lib_out_dir = out_dir.Dir('lib').Dir(lib) - output = do_sconscript(env, LIB_DIR.Dir(lib), lib_out_dir) - if not output: - print "Lib {} didn't return any object".format(lib) - env['AVAILABLE_LIBS'][lib] = output - env.Append(LIBPATH=[lib_out_dir]) - - # Get source files. - src_out_dir = out_dir.Dir('src') - do_sconscript(env, SRC_DIR, src_out_dir) - env.Append(LIBPATH=[src_out_dir], - CPPPATH=[SRC_DIR]) - - # Get test binaries. - test = do_sconscript(env, TEST_DIR, out_dir.Dir('test')) - run_tests = env.Alias('test', [test], '{} --gtest_color=yes'.format(test[0].path)) - env.AlwaysBuild(run_tests) + env = Environment.for_mode(mode) + env.process_lib_dirs() + env.process_src() + env.process_tests() diff --git a/lib/gtest/SConscript b/lib/gtest/SConscript index 07126f5..5d2e1c7 100644 --- a/lib/gtest/SConscript +++ b/lib/gtest/SConscript @@ -1,18 +1,8 @@ # SConscript # vim: set ft=python: -# -# Build file for the gtest library. This file also serves as an example for how -# to build libraries for inclusion with my SCons environment. Libraries should -# return a build object. -# # Eryn Wells -import os.path - -Import('env') -env.Append(CPPPATH=[Dir('include').srcnode()]) - -files = [ +Library('gtest', [ 'gtest-all.cc', 'gtest-death-test.cc', 'gtest-filepath.cc', @@ -21,7 +11,5 @@ files = [ 'gtest-test-part.cc', 'gtest-typed-test.cc', 'gtest.cc', -] - -gtest = env.Library('gtest', files) -Return('gtest') + 'gtest_main.cc' +]) diff --git a/site_scons/erw.py b/site_scons/erw.py deleted file mode 100644 index 224e4d6..0000000 --- a/site_scons/erw.py +++ /dev/null @@ -1,81 +0,0 @@ -# erw.py -# Eryn Wells - -import sys - -import SCons.Environment - -import paths - -# -# Environments -# - -class Environment(SCons.Environment.Environment): - ''' - Default SCons environment for building things. - ''' - - def __init__(self, name, modern=True, paranoid=True, colorful=True, succinct=True, **kwargs): - super(Environment, self).__init__(**kwargs) - - self['NAME'] = name - - if 'CC' not in kwargs: - self['CC'] = self.Detect(['clang', 'gcc']) - if 'CXX' not in kwargs: - self['CXX'] = self.Detect(['clang++', 'g++']) - if 'LINK' not in kwargs: - self['LINK'] = self.Detect(['clang++', 'clang', 'ld']) - - # Modern C/C++ - if modern: - self.Append(CFLAGS=['-std=c99']) - self.Append(CXXFLAGS=['-std=c++11']) - - # Paranoid C/C++ - if paranoid: - self.Append(CCFLAGS=['-Wall', '-Wextra', '-pedantic']) - - # Colorful C/C++ - if colorful and sys.stdout.isatty(): - if 'clang' in self['CC'] or 'clang' in self['CXX']: - self.Append(CCFLAGS=['-fcolor-diagnostics']) - - self['ARCOMSTR'] = self._comstr('Archiving', succinct) - self['ASCOMSTR'] = self._comstr('Assembling', succinct) - self['ASPPCOMSTR'] = self._comstr('Assembling', succinct) - self['CCCOMSTR'] = self._comstr('Building (C)', succinct) - self['CXXCOMSTR'] = self._comstr('Building (C++)', succinct) - self['LINKCOMSTR'] = self._comstr('Linking', succinct) - self['RANLIBCOMSTR'] = self._comstr('Indexing', succinct) - self['SHCCCOMSTR'] = self._comstr('Building (C, Shared)', succinct) - self['SHCXXCOMSTR'] = self._comstr('Building (C++, Shared)', succinct) - self['SHLINKCOMSTR'] = self._comstr('Linking (Shared)', succinct) - - def _comstr(self, action, succinct=True): - if succinct: - return '{:>25}: $TARGET'.format(action) - else: - return ' [{:^6}] $TARGET'.format(action) - - -class DebugEnvironment(Environment): - def __init__(self, name='debug', **kwargs): - super(DebugEnvironment, self).__init__(name, **kwargs) - self.Append(CPPDEFINES=['DEBUG']) - self.Append(CCFLAGS=['-O0', '-g']) - - -class BetaEnvironment(Environment): - def __init__(self, name='beta', **kwargs): - super(BetaEnvironment, self).__init__(name, **kwargs) - self.Append(CPPDEFINES=['DEBUG']) - self.Append(CCFLAGS=['-O3', '-g']) - - -class ReleaseEnvironment(Environment): - def __init__(self, name='release', **kwargs): - super(ReleaseEnvironment, self).__init__(name, **kwargs) - self.Append(CPPDEFINES=['NDEBUG', 'RELEASE']) - self.Append(CCFLAGS=['-O3']) diff --git a/site_scons/site_init.py b/site_scons/site_init.py index d1412f0..e451523 100644 --- a/site_scons/site_init.py +++ b/site_scons/site_init.py @@ -1,6 +1,14 @@ # site_init.py # Eryn Wells +import logging +import os +import sys + +import SCons.Environment +import SCons.Errors + +import paths def do_sconscript(env, src_dir, out_dir=None): ''' @@ -13,6 +21,13 @@ def do_sconscript(env, src_dir, out_dir=None): kwargs['variant_dir'] = out_dir return env.SConscript(sconscript, {'env': env}, **kwargs) + +def setup_logging(level=logging.DEBUG): + '''Configure global logging for the SCons system.''' + root = logging.getLogger() + root.setLevel(logging.DEBUG) + root.addHandler(logging.StreamHandler()) + # # Argument utils # @@ -28,3 +43,204 @@ def get_bool_argument(arg): except ValueError: pass return str(arg).lower() != 'false' + +# +# Builders +# + +def program_builder(env): + original_builder = env.Program + def builder(env, prog_name, sources, *args, **kwargs): + return original_builder(prog_name, sources, *args, **kwargs) + return builder + +# +# Environments +# + +class Environment(SCons.Environment.Environment): + ''' + Default SCons environment for building things. + ''' + + @classmethod + def _comstr(cls, action, succinct=True): + if succinct: + return '{:>25}: $TARGET'.format(action) + # Use the default *COMSTR. + return None + + @classmethod + def for_mode(cls, mode, name=None, succinct=True): + kwargs = {'succinct': succinct} + if name: + kwargs['name'] = name + mode_lower = mode.lower() + if mode_lower == 'debug': + return DebugEnvironment(**kwargs) + elif mode_lower == 'beta': + return BetaEnvironment(**kwargs) + elif mode_lower == 'release': + return ReleaseEnvironment(**kwargs) + raise SCons.Errors.UserError('Invalid mode: {}'.format(mode)) + + def __init__(self, name, modern=True, paranoid=True, colorful=True, succinct=True, **kwargs): + super(Environment, self).__init__(**self._append_custom_tools(kwargs)) + + self.SetDefault(NAME=name) + + self.SetDefault(LOGGER=logging.getLogger(name)) + + self.SetDefault(BUILD_ROOT=self.Dir('#build')) + self.SetDefault(LIB_ROOT=self.Dir('#lib')) + self.SetDefault(SRC_ROOT=self.Dir('#src')) + self.SetDefault(TEST_ROOT=self.Dir('#test')) + self.SetDefault(INCLUDE_ROOT=self.Dir('#include')) + + # Allow same directory includes. + self.AppendUnique(CPPPATH=['.']) + + self.SetDefault(CC=self.Detect(['clang', 'gcc'])) + self.SetDefault(CXX=self.Detect(['clang++', 'g++'])) + self.SetDefault(LINK=self.Detect(['clang++', 'clang', 'ld'])) + + # Modern C/C++ + if modern: + self.AppendUnique(CFLAGS=['-std=c99']) + self.AppendUnique(CXXFLAGS=['-std=c++11']) + + # Paranoid C/C++ + if paranoid: + self.AppendUnique(CCFLAGS=['-Wall', '-Wextra', '-pedantic']) + + # Colorful C/C++ + if colorful and sys.stdout.isatty(): + if 'clang' in self['CC'] or 'clang' in self['CXX']: + self.AppendUnique(CCFLAGS=['-fcolor-diagnostics']) + + # Pretty printing + self.SetDefault(ARCOMSTR=Environment._comstr('Archiving', succinct)) + self.SetDefault(ASCOMSTR=Environment._comstr('Assembling', succinct)) + self.SetDefault(ASPPCOMSTR=Environment._comstr('Assembling', succinct)) + self.SetDefault(CCCOMSTR=Environment._comstr('Building (C)', succinct)) + self.SetDefault(CXXCOMSTR=Environment._comstr('Building (C++)', succinct)) + self.SetDefault(LINKCOMSTR=Environment._comstr('Linking', succinct)) + self.SetDefault(RANLIBCOMSTR=Environment._comstr('Indexing', succinct)) + self.SetDefault(SHCCCOMSTR=Environment._comstr('Building (C, Shared)', succinct)) + self.SetDefault(SHCXXCOMSTR=Environment._comstr('Building (C++, Shared)', succinct)) + self.SetDefault(SHLINKCOMSTR=Environment._comstr('Linking (Shared)', succinct)) + + @property + def build_root(self): + '''Return the build output directory for this environment.''' + return self['BUILD_ROOT'].Dir(self['NAME']) + + @property + def lib_root(self): + '''Return the root directory for libraries for this environment.''' + return self['LIB_ROOT'] + + @property + def src_root(self): + '''Return the main source root directory for this environment.''' + return self['SRC_ROOT'] + + @property + def lib_dirs(self): + for lib in os.listdir(self['LIB_ROOT'].abspath): + lib_dir = self['LIB_ROOT'].Dir(lib) + if not lib_dir.isdir(): + continue + yield (lib, lib_dir) + + # + # Library processing + # + + def process_lib_dirs(self): + self.log('Processing libs in #{} ...'.format(self.lib_root.path)) + for name, lib in self.lib_dirs: + self.log(' - {}'.format(name)) + self.LibDir(name) + + def process_src(self): + out_dir = self.build_root.Dir('src') + # TODO: Do the thing. + # do_sconscript(env, env.source_root, src_out_dir) + self.Append(CPPPATH=[self.src_root]) + + def lib(self, name): + return self['LIBS'].get(name) + + def register_lib(self, name, lib): + if name in self['LIBS']: + self.log_error('Library has already been built: {}'.format(name)) + self['LIBS'][name] = lib + + # + # Test processing + # + + def test_objects(self, name): + self._ensure_test_structure(name) + return self['TESTS'][name]['objects'] + + def test_program(self, name): + self._ensure_test_structure(name) + return self['TESTS'][name]['program'] + + def register_test_program(self, name, prog): + self._ensure_test_structure(name) + self['TESTS'][name]['program'] = prog + + def process_tests(self): + for test, struct in self['TESTS'].iteritems(): + if not struct['program']: + continue + self.TestRun(test) + + def _ensure_test_structure(self, name): + self['TESTS'].setdefault(name, {'program': None, 'objects': []}) + + # + # Logging + # + + def log(self, msg, *args, **kwargs): + self['LOGGER'].info(msg, *args, **kwargs) + + def log_debug(self, msg, *args, **kwargs): + self['LOGGER'].debug(msg, *args, **kwargs) + + def log_error(self, msg, *args, **kwargs): + self['LOGGER'].error(msg, *args, **kwargs) + + def _append_custom_tools(self, kwargs): + '''Add custom tools to the `kwargs`.''' + tools = kwargs.setdefault('tools', ['default']) + for tool in ['lib', 'test']: + if tool in tools: + continue + tools.append(tool) + return kwargs + + +class DebugEnvironment(Environment): + def __init__(self, name='debug', **kwargs): + super(DebugEnvironment, self).__init__(name, **kwargs) + self.Append(CPPDEFINES=['DEBUG']) + self.Append(CCFLAGS=['-O0', '-g']) + + +class BetaEnvironment(Environment): + def __init__(self, name='beta', **kwargs): + super(BetaEnvironment, self).__init__(name, **kwargs) + self.Append(CPPDEFINES=['DEBUG']) + self.Append(CCFLAGS=['-O3', '-g']) + + +class ReleaseEnvironment(Environment): + def __init__(self, name='release', **kwargs): + super(ReleaseEnvironment, self).__init__(name, **kwargs) + self.Append(CPPDEFINES=['NDEBUG', 'RELEASE']) + self.Append(CCFLAGS=['-O3']) diff --git a/site_scons/site_tools/lib.py b/site_scons/site_tools/lib.py new file mode 100644 index 0000000..212bd6b --- /dev/null +++ b/site_scons/site_tools/lib.py @@ -0,0 +1,59 @@ +# lib.py +# Eryn Wells + +''' +SCons builder for a lib directory. +''' + +import SCons.Errors +import SCons.Script + + +def _process_lib_dir(env, lib, src_dir=None, out_dir=None, inc_dir=None): + if not src_dir: + src_dir = env.lib_root.Dir(lib) + if not src_dir.isdir(): + err = 'Invalid library source directory: {}'.format(src_dir) + env.log_error(err) + raise SCons.Errors.UserError(err) + if not out_dir: + out_dir = env.build_root.Dir('lib').Dir(lib) + if not inc_dir: + include_dir = src_dir.Dir('include') + if include_dir.isdir(): + inc_dir = [include_dir] + env.Append(CPPPATH=inc_dir) + exports = {'Library': env.Library, + 'StaticLibrary': env.StaticLibrary, + 'SharedLibrary': env.SharedLibrary} + SCons.Script._SConscript.GlobalDict.update(exports) + out = env.SConscript(src_dir.File('SConscript'), + {'env': env.Clone()}, + variant_dir=out_dir, + exports=exports) + return out + + +def _build_library(env, lib_func): + original_builder = lib_func + + def builder(env, lib_name, sources, *args, **kwargs): + lib = original_builder(lib_name, sources, *args, **kwargs) + env.register_lib(lib_name, lib) + return lib + + return builder + +# +# SCons tool interface +# + +def generate(env): + env.SetDefault(LIBS={}) + env.AddMethod(_process_lib_dir, 'LibDir') + env.AddMethod(_build_library(env, env.Library), 'Library') + env.AddMethod(_build_library(env, env.StaticLibrary), 'StaticLibrary') + env.AddMethod(_build_library(env, env.SharedLibrary), 'SharedLibrary') + +def exists(env): + return True diff --git a/site_scons/site_tools/test.py b/site_scons/site_tools/test.py new file mode 100644 index 0000000..df8e924 --- /dev/null +++ b/site_scons/site_tools/test.py @@ -0,0 +1,51 @@ +# test.py +# Eryn Wells + +''' +Test builder for SCons. Test files are compiled to objects and stored in the +environment. +''' + +def _process_test_dir(env, dir, program=None): + # TODO: Builder for test directories? + pass + + +def _build_test_object(env, source, program=None): + obj = env.Object(source) + if not program: + program = 'test' + try: + env.test_objects(program).extend(obj) + except TypeError: + env.test_objects(program).append(obj) + return obj + + +def _build_test_program(env, name=None): + if not name: + name = 'test' + prog = env.Program(name, env.test_objects(name), LIBS=[env.lib('gtest')]) + env.register_test_program(name, prog) + return + + +def _run_tests(env, name=None): + if not name: + name = 'test' + cmd = env.Command(env.test_program(name), None, '$SOURCE --gtest_color=yes') + env.AlwaysBuild(cmd) + return cmd + +# +# SCons tool interface +# + +def generate(env): + env.SetDefault(TESTS={}) + env.AddMethod(_build_test_object, 'Test') + env.AddMethod(_build_test_program, 'TestProgram') + env.AddMethod(_run_tests, 'TestRun') + +def exists(env): + return 'Object' in env