diff --git a/.gitignore b/.gitignore index 58eb4a76..42afad93 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ Makefile.dep ALL_BUILD.* INSTALL.* ZERO_CHECK.* +CMakeLists.txt # Visual Studio *.obj @@ -333,3 +334,6 @@ __pycache__ .history/* .cache/* enc_temp_folder/ + +# KDevelop4 +*.kdev4 diff --git a/scripts/waifulib/cmake.py b/scripts/waifulib/cmake.py new file mode 100644 index 00000000..5ab4aaa9 --- /dev/null +++ b/scripts/waifulib/cmake.py @@ -0,0 +1,410 @@ +#! /usr/bin/env python +# -*- encoding: utf-8 -*- +# Licensed under The MIT License (MIT) +# Copyright (c) 2016, Michel Mooij +# Copyright (c) 2023, Velaron + +''' +Summary +------- +Generate *cmake* files of all C/C++ programs, static- and shared libraries +that have been defined within a *waf* build environment. +Once exported to *cmake*, all exported (C/C++) tasks can be build without +any further need for, or dependency, to the *waf* build system itself. + +**cmake** is an open source cross-platform build system designed to build, test +and package software. It is available for all major Desktop Operating Systems +(MS Windows, all major Linux distributions and Macintosh OS-X). +See http://www.cmake.org for a more detailed description on how to install +and use it for your particular Desktop environment. + + +Description +----------- +When exporting *waf* project data, a single top level **CMakeLists.txt** file +will be exported in the top level directory of your *waf* build environment. +This *cmake* build file will contain references to all exported *cmake* +build files of each individual C/C++ build task. It will also contain generic +variables and settings (e.g compiler to use, global preprocessor defines, link +options and so on). + +Example below presents an overview of an environment in which *cmake* +build files already have been exported:: + + . + ├── components + │ └── clib + │ ├── program + │ │ ├── CMakeLists.txt + │ │ └── wscript + │ ├── shared + │ │ ├── CMakeLists.txt + │ │ └── wscript + │ └── static + │ ├── CMakeLists.txt + │ └── wscript + │ + ├── CMakeLists.txt + └── wscript + + +Usage +----- +Tasks can be exported to *cmake* using the command, as shown in the +example below:: + + $ waf cmake + +All exported *cmake* build files can be removed in 'one go' using the *cmake* +*cleanup* option:: + + $ waf cmake --cmake-clean + +Tasks generators to be excluded can be marked with the *skipme* option +as shown below:: + + def build(bld): + bld.program(name='foo', src='foobar.c', cmake_skip=True) + +''' + + +from waflib.Build import BuildContext +from waflib import Utils, Logs, Context, Errors + + +def get_deps(bld, target): + '''Returns a list of (nested) targets on which this target depends. + + :param bld: a *waf* build instance from the top level *wscript* + :type bld: waflib.Build.BuildContext + :param target: task name for which the dependencies should be returned + :type target: str + :returns: a list of task names on which the given target depends + ''' + try: + tgen = bld.get_tgen_by_name(target) + except Errors.WafError: + return [] + else: + uses = Utils.to_list(getattr(tgen, 'use', [])) + deps = uses[:] + for use in uses: + deps += get_deps(bld, use) + return list(set(deps)) + + +def get_tgens(bld, names): + '''Returns a list of task generators based on the given list of task + generator names. + + :param bld: a *waf* build instance from the top level *wscript* + :type bld: waflib.Build.BuildContext + :param names: list of task generator names + :type names: list of str + :returns: list of task generators + ''' + tgens = [] + for name in names: + try: + tgen = bld.get_tgen_by_name(name) + except Errors.WafError: + pass + else: + tgens.append(tgen) + return list(set(tgens)) + + +def get_targets(bld): + '''Returns a list of user specified build targets or None if no specific + build targets has been selected using the *--targets=* command line option. + + :param bld: a *waf* build instance from the top level *wscript*. + :type bld: waflib.Build.BuildContext + :returns: a list of user specified target names (using --targets=x,y,z) or None + ''' + if bld.targets == '': + return None + targets = bld.targets.split(',') + for target in targets: + targets += get_deps(bld, target) + return targets + + +def options(opt): + '''Adds command line options for the CMake *waftool*. + + :param opt: Options context from the *waf* build environment. + :type opt: waflib.Options.OptionsContext + ''' + opt.add_option('--cmake', dest='cmake', default=False, + action='store_true', help='select cmake for export/import actions') + opt.add_option('--clean', dest='clean', default=False, + action='store_true', help='delete exported files') + + +def configure(conf): + '''Method that will be invoked by *waf* when configuring the build + environment. + + :param conf: Configuration context from the *waf* build environment. + :type conf: waflib.Configure.ConfigurationContext + ''' + conf.find_program('cmake', var='CMAKE', mandatory=False) + + +class CMakeContext(BuildContext): + '''export C/C++ tasks to CMake.''' + cmd = 'cmake' + + def execute(self): + '''Will be invoked when issuing the *cmake* command.''' + self.restore() + if not self.all_envs: + self.load_envs() + self.recurse([self.run_dir]) + self.pre_build() + + for group in self.groups: + for tgen in group: + try: + f = tgen.post + except AttributeError: + pass + else: + f() + try: + self.get_tgen_by_name('') + except Exception: + pass + + self.cmake = True + if self.options.clean: + cleanup(self) + else: + export(self) + self.timer = Utils.Timer() + + +def export(bld): + '''Exports all C and C++ task generators to cmake. + + :param bld: a *waf* build instance from the top level *wscript*. + :type bld: waflib.Build.BuildContext + ''' + if not bld.options.cmake and not hasattr(bld, 'cmake'): + return + + cmakes = {} + loc = bld.path.relpath().replace('\\', '/') + top = CMake(bld, loc) + cmakes[loc] = top + targets = get_targets(bld) + + for tgen in bld.task_gen_cache_names.values(): + if targets and tgen.get_name() not in targets: + continue + if getattr(tgen, 'cmake_skip', False): + continue + if set(('c', 'cxx')) & set(getattr(tgen, 'features', [])): + loc = tgen.path.relpath().replace('\\', '/') + if loc not in cmakes: + cmake = CMake(bld, loc) + cmakes[loc] = cmake + top.add_child(cmake) + cmakes[loc].add_tgen(tgen) + + for cmake in cmakes.values(): + cmake.export() + + +def cleanup(bld): + '''Removes all generated makefiles from the *waf* build environment. + + :param bld: a *waf* build instance from the top level *wscript*. + :type bld: waflib.Build.BuildContext + ''' + if not bld.options.clean: + return + + loc = bld.path.relpath().replace('\\', '/') + CMake(bld, loc).cleanup() + targets = get_targets(bld) + + for tgen in bld.task_gen_cache_names.values(): + if targets and tgen.get_name() not in targets: + continue + if getattr(tgen, 'cmake_skip', False): + continue + if set(('c', 'cxx')) & set(getattr(tgen, 'features', [])): + loc = tgen.path.relpath().replace('\\', '/') + CMake(bld, loc).cleanup() + + +class CMake(object): + def __init__(self, bld, location): + self.bld = bld + self.location = location + self.cmakes = [] + self.tgens = [] + + def export(self): + content = self.get_content() + if not content: + return + + node = self.make_node() + if not node: + return + node.write(content) + Logs.pprint('YELLOW', 'exported: %s' % node.abspath()) + + def cleanup(self): + node = self.find_node() + if node: + node.delete() + Logs.pprint('YELLOW', 'removed: %s' % node.abspath()) + + def add_child(self, cmake): + self.cmakes.append(cmake) + + def add_tgen(self, tgen): + self.tgens.append(tgen) + + def get_location(self): + return self.location + + def get_fname(self): + name = '%s/CMakeLists.txt' % (self.location) + return name + + def find_node(self): + name = self.get_fname() + if not name: + return None + return self.bld.srcnode.find_node(name) + + def make_node(self): + name = self.get_fname() + if not name: + return None + return self.bld.srcnode.make_node(name) + + def get_content(self): + is_top = (self.location == self.bld.path.relpath()) + + content = '' + if is_top: + content += 'cmake_minimum_required(VERSION 2.8.12)\n' + content += 'project(%s)\n' % (getattr(Context.g_module, + Context.APPNAME)) + content += '\n' + + env = self.bld.env + defines = env.DEFINES + if len(defines): + content += 'add_definitions(\n -D%s\n)\n' % ( + '\n -D'.join(defines)) + content += '\n' + + flags = env.CFLAGS + if len(flags): + content += 'set(CMAKE_C_FLAGS "%s")\n' % (' '.join(flags)) + + flags = env.CXXFLAGS + if len(flags): + content += 'set(CMAKE_CXX_FLAGS "%s")\n' % (' '.join(flags)) + + if len(self.tgens): + content += '\n' + for tgen in self.tgens: + content += self.get_tgen_content(tgen) + + if len(self.cmakes): + content += '\n' + for cmake in self.cmakes: + content += 'add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/%s)\n' % ( + cmake.get_location()) + + return content + + def get_tgen_content(self, tgen): + content = '' + name = tgen.get_name() + + content += 'set(%s_SRC' % (name.upper()) + for src in tgen.source: + content += '\n %s' % (src.path_from(tgen.path).replace('\\', '/')) + content += '\n)\n\n' + + includes = self.get_includes(tgen) + # includes.extend(tgen.env.INCLUDES) + if len(includes): + content += 'set(%s_INCLUDES' % (name.upper()) + for include in includes: + content += '\n %s' % include + content += '\n)\n\n' + content += 'include_directories(${%s_INCLUDES})\n' % (name.upper()) + + link_dirs = getattr(tgen.env, 'LIBPATH', []) + if len(link_dirs): + content += '\nlink_directories(' + for dir in link_dirs: + content += '\n \"%s\"' % dir.replace('\\', '/') + content += '\n)\n\n' + + if set(('cprogram', 'cxxprogram')) & set(tgen.features): + if tgen.env.DEST_OS == 'win32': + content += 'add_executable(%s WIN32 ${%s_SRC})\n' % ( + name, name.upper()) + else: + content += 'add_executable(%s ${%s_SRC})\n' % (name, + name.upper()) + + elif set(('cshlib', 'cxxshlib')) & set(tgen.features): + content += 'add_library(%s SHARED ${%s_SRC})\n\n' % ( + name, name.upper()) + + else: # cstlib, cxxstlib or objects + content += 'add_library(%s ${%s_SRC})\n\n' % (name, name.upper()) + + defines = self.get_genlist(tgen, 'defines') + defines.extend(tgen.env.DEFINES) + if len(defines): + content += 'target_compile_definitions(%s PRIVATE\n -D%s\n)\n' % ( + name, '\n -D'.join(defines)) + content += '\n' + + libs = getattr(tgen.env, 'LIB', []) + libs.extend(tgen.env.STLIB) + + if len(libs): + content += '\n' + content += 'target_link_libraries(%s\n %s)\n' % (name, '\n '.join(libs)) + content += '\n' + + return content + + def get_includes(self, tgen): + '''returns the include paths for the given task generator. + ''' + includes = self.get_genlist(tgen, 'includes') + for use in getattr(tgen, 'use', []): + key = 'INCLUDES_%s' % use + try: + tg = self.bld.get_tgen_by_name(use) + if 'fake_lib' in tg.features: + if key in tgen.env: + includes.extend([l.replace('\\', '/') + for l in tgen.env[key]]) + except Errors.WafError: + if key in tgen.env: + includes.extend([l.replace('\\', '/') + for l in tgen.env[key]]) + return includes + + def get_genlist(self, tgen, name): + lst = Utils.to_list(getattr(tgen, name, [])) + lst = [str(l.path_from(tgen.path)) if hasattr( + l, 'path_from') else l for l in lst] + return [l.replace('\\', '/') for l in lst] diff --git a/wscript b/wscript index 4838ee9c..0e319594 100644 --- a/wscript +++ b/wscript @@ -100,7 +100,7 @@ REFDLLS = [ ] def options(opt): - opt.load('reconfigure compiler_optimizations xshlib xcompile compiler_cxx compiler_c sdl2 clang_compilation_database strip_on_install waf_unit_test msdev msvs msvc subproject') + opt.load('reconfigure compiler_optimizations xshlib xcompile compiler_cxx compiler_c sdl2 clang_compilation_database strip_on_install waf_unit_test msdev msvs msvc subproject cmake') grp = opt.add_option_group('Common options') @@ -176,7 +176,7 @@ def configure(conf): if conf.env.COMPILER_CC == 'msvc': conf.load('msvc_pdb') - conf.load('msvs msdev subproject gitversion clang_compilation_database strip_on_install waf_unit_test enforce_pic') + conf.load('msvs msdev subproject gitversion clang_compilation_database strip_on_install waf_unit_test enforce_pic cmake') # Force XP compatibility, all build targets should add subsystem=bld.env.MSVC_SUBSYSTEM if conf.env.MSVC_TARGETS[0] == 'x86':