#! /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]