#! /usr/bin/env python
# -*- encoding: utf-8 -*-
# Michel Mooij, michel.mooij7@gmail.com
'''
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
import waftools
from waftools import deps
[docs]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')
[docs]class CMakeContext(BuildContext):
'''export C/C++ tasks to CMake.'''
cmd = 'cmake'
[docs] 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()
[docs]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 = waftools.deps.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_skipme', 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()
[docs]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 = waftools.deps.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_skipme', False):
continue
if set(('c', 'cxx')) & set(getattr(tgen, 'features', [])):
loc = tgen.path.relpath().replace('\\', '/')
CMake(bld, loc).cleanup()
[docs]class CMake(object):
def __init__(self, bld, location):
self.bld = bld
self.location = location
self.cmakes = []
self.tgens = []
[docs] 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())
[docs] def cleanup(self):
node = self.find_node()
if node:
node.delete()
Logs.pprint('YELLOW', 'removed: %s' % node.abspath())
[docs] def add_child(self, cmake):
self.cmakes.append(cmake)
[docs] def add_tgen(self, tgen):
self.tgens.append(tgen)
[docs] def get_location(self):
return self.location
[docs] def get_fname(self):
name = '%s/CMakeLists.txt' % (self.location)
return name
[docs] def find_node(self):
name = self.get_fname()
if not name:
return None
return self.bld.srcnode.find_node(name)
[docs] def make_node(self):
name = self.get_fname()
if not name:
return None
return self.bld.srcnode.make_node(name)
[docs] def get_content(self):
is_top = (self.location == self.bld.path.relpath())
content = ''
if is_top:
content += 'cmake_minimum_required (VERSION 2.6)\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(-D%s)\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
[docs] def get_tgen_content(self, tgen):
content = ''
name = tgen.get_name()
content += 'set(%s_SOURCES' % (name)
for src in tgen.source:
content += '\n %s' % (src.path_from(tgen.path).replace('\\','/'))
content += ')\n\n'
includes = self.get_includes(tgen)
includes.extend(tgen.env.INCLUDES)
if len(includes):
content += 'set(%s_INCLUDES' % (name)
for include in includes:
content += '\n %s' % include
content += ')\n\n'
content += 'include_directories(${%s_INCLUDES})\n' % (name)
content += '\n'
defines = self.get_genlist(tgen, 'defines')
if len(defines):
content += 'add_definitions(-D%s)\n' % (' -D'.join(defines))
content += '\n'
if set(('cprogram', 'cxxprogram')) & set(tgen.features):
content += 'add_executable(%s ${%s_SOURCES})\n' % (name, name)
elif set(('cshlib', 'cxxshlib')) & set(tgen.features):
content += 'add_library(%s SHARED ${%s_SOURCES})\n' % (name, name)
else: # cstlib, cxxstlib or objects
content += 'add_library(%s ${%s_SOURCES})\n' % (name, name)
libs = getattr(tgen, 'use', []) + getattr(tgen, 'lib', [])
if len(libs):
content += '\n'
for lib in libs:
content += 'target_link_libraries(%s %s)\n' % (name, lib)
content += '\n'
return content
[docs] 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(tgen.env[key])
except Errors.WafError:
if key in tgen.env:
includes.extend(tgen.env[key])
return includes
[docs] 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]