#!/usr/bin/env python import os import sys from collections import OrderedDict, namedtuple # we do not use the nicer sys.version_info.major # for compatibility with Python < 2.7 if sys.version_info[0] > 2: from io import StringIO from configparser import RawConfigParser import urllib.request class URLopener(urllib.request.FancyURLopener): def http_error_default(self, url, fp, errcode, errmsg, headers): sys.stderr.write("ERROR: could not fetch %s\n" % url) sys.exit(-1) else: from StringIO import StringIO from ConfigParser import RawConfigParser import urllib class URLopener(urllib.FancyURLopener): def http_error_default(self, url, fp, errcode, errmsg, headers): sys.stderr.write("ERROR: could not fetch %s\n" % url) sys.exit(-1) AUTOCMAKE_GITHUB_URL = 'https://github.com/scisoft/autocmake' # ------------------------------------------------------------------------------ def fetch_url(src, dst): """ Fetch file from URL src and save it to dst. """ dirname = os.path.dirname(dst) if dirname != '': if not os.path.isdir(dirname): os.makedirs(dirname) opener = URLopener() opener.retrieve(src, dst) # ------------------------------------------------------------------------------ def print_progress_bar(text, done, total, width): """ Print progress bar. """ n = int(float(width) * float(done) / float(total)) sys.stdout.write("\r%s [%s%s] (%i/%i)" % (text, '#' * n, ' ' * (width - n), done, total)) sys.stdout.flush() # ------------------------------------------------------------------------------ def align_options(options): """ Indents flags and aligns help texts. """ l = 0 for opt in options: if len(opt[0]) > l: l = len(opt[0]) s = [] for opt in options: s.append(' %s%s %s' % (opt[0], ' ' * (l - len(opt[0])), opt[1])) return '\n'.join(s) # ------------------------------------------------------------------------------ def gen_cmake_command(config): """ Generate CMake command. """ s = [] s.append("\n\ndef gen_cmake_command(options, arguments):") s.append(' """') s.append(" Generate CMake command based on options and arguments.") s.append(' """') s.append(" command = []") # take care of environment variables for section in config.sections(): if config.has_option(section, 'export'): for env in config.get(section, 'export').split('\n'): s.append(' command.append(%s)' % env) s.append(" command.append('cmake')") # take care of cmake definitions for section in config.sections(): if config.has_option(section, 'define'): for definition in config.get(section, 'define').split('\n'): s.append(' command.append(%s)' % definition) s.append(" command.append('-DCMAKE_BUILD_TYPE=%s' % arguments['--type'])") s.append(" command.append('-G \"%s\"' % arguments['--generator'])") s.append(" if(arguments['--cmake-options']):") s.append(" command.append('%s' % arguments['--cmake-options'])") s.append("\n return ' '.join(command)") return '\n'.join(s) # ------------------------------------------------------------------------------ def autogenerated_notice(): s = [] s.append('# This file is autogenerated by Autocmake') s.append('# Copyright (c) 2015 by Radovan Bast and Jonas Juselius') s.append('# See https://github.com/scisoft/autocmake/') return '\n'.join(s) # ------------------------------------------------------------------------------ def gen_setup(config, relative_path): """ Generate setup.py script. """ s = [] s.append('#!/usr/bin/env python') s.append('\n%s' % autogenerated_notice()) s.append('\nimport os') s.append('import sys') s.append("\nsys.path.append('%s')" % os.path.join(relative_path, 'lib')) s.append('from config import configure') s.append('import docopt') s.append('\n\noptions = """') s.append('Usage:') s.append(' ./setup.py [options] []') s.append(' ./setup.py (-h | --help)') s.append('\nOptions:') options = [] for section in config.sections(): if config.has_option(section, 'docopt'): for opt in config.get(section, 'docopt').split('\n'): first = opt.split()[0].strip() rest = ' '.join(opt.split()[1:]).strip() options.append([first, rest]) options.append(['--type=', 'Set the CMake build type (debug, release, or relwithdeb) [default: release].']) options.append(['--generator=', 'Set the CMake build system generator [default: Unix Makefiles].']) options.append(['--show', 'Show CMake command and exit.']) options.append(['--cmake-options=', 'Define options to CMake [default: None].']) options.append(['', 'Build directory.']) options.append(['-h --help', 'Show this screen.']) s.append(align_options(options)) s.append('"""') s.append(gen_cmake_command(config)) s.append("\n\ntry:") s.append(" arguments = docopt.docopt(options, argv=None)") s.append("except docopt.DocoptExit:") s.append(r" sys.stderr.write('ERROR: bad input to %s\n' % sys.argv[0])") s.append(" sys.stderr.write(options)") s.append(" sys.exit(-1)") s.append("\nroot_directory = os.path.dirname(os.path.realpath(__file__))") s.append("build_path = arguments['']") s.append("cmake_command = '%s %s' % (gen_cmake_command(options, arguments), root_directory)") s.append("configure(root_directory, build_path, cmake_command, arguments['--show'])") return s # ------------------------------------------------------------------------------ def gen_cmakelists(config, relative_path, modules): """ Generate CMakeLists.txt. """ if not config.has_option('project', 'name'): sys.stderr.write("ERROR: you have to specify the project name\n") sys.stderr.write(" in autocmake.cfg under [project]\n") sys.exit(-1) project_name = config.get('project', 'name') s = [] s.append(autogenerated_notice()) s.append('\n# set minimum cmake version') s.append('cmake_minimum_required(VERSION 2.8 FATAL_ERROR)') s.append('\n# project name') s.append('project(%s)' % project_name) s.append('\n# do not rebuild if rules (compiler flags) change') s.append('set(CMAKE_SKIP_RULE_DEPENDENCY TRUE)') s.append('\n# if CMAKE_BUILD_TYPE undefined, we set it to Debug') s.append('if(NOT CMAKE_BUILD_TYPE)') s.append(' set(CMAKE_BUILD_TYPE "Debug")') s.append('endif()') if len(modules) > 0: s.append('\n# directories which hold included cmake modules') for directory in set([module.path for module in modules]): rel_cmake_module_path = os.path.join(relative_path, directory) # on windows cmake corrects this so we have to make it wrong again rel_cmake_module_path = rel_cmake_module_path.replace('\\', '/') s.append('set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${PROJECT_SOURCE_DIR}/%s)' % rel_cmake_module_path) if len(modules) > 0: s.append('\n# included cmake modules') for module in modules: s.append('include(%s)' % os.path.splitext(module.name)[0]) return s # ------------------------------------------------------------------------------ def fetch_modules(config, relative_path): """ Assemble modules which will be included in CMakeLists.txt. """ download_directory = 'downloaded' if not os.path.exists(download_directory): os.makedirs(download_directory) l = list(filter(lambda x: config.has_option(x, 'source'), config.sections())) n = len(l) modules = [] Module = namedtuple('Module', 'path name') if n > 0: # otherwise division by zero in print_progress_bar i = 0 print_progress_bar(text='- assembling modules:', done=0, total=n, width=30) for section in config.sections(): if config.has_option(section, 'source'): for src in config.get(section, 'source').split('\n'): module_name = os.path.basename(src) if 'http' in src: path = download_directory name = 'autocmake_%s' % module_name dst = os.path.join(download_directory, 'autocmake_%s' % module_name) fetch_url(src, dst) file_name = dst else: if os.path.exists(src): path = os.path.dirname(src) name = module_name file_name = src else: sys.stderr.write("ERROR: %s does not exist\n" % src) sys.exit(-1) # unless config is 'custom' we infer configuration # from the module documentation parse_doc = True if config.has_option(section, 'config'): if config.get(section, 'config') == 'custom': parse_doc = False if parse_doc: with open(file_name, 'r') as f: config_docopt, config_define, config_export, config_fetch = parse_cmake_module(f.read()) if config_docopt: config.set(section, 'docopt', config_docopt) if config_define: config.set(section, 'define', config_define) if config_export: config.set(section, 'export', config_export) if config_fetch: config.set(section, 'fetch', config_fetch) modules.append(Module(path=path, name=name)) i += 1 print_progress_bar( text='- assembling modules:', done=i, total=n, width=30 ) if config.has_option(section, 'fetch'): for src in config.get(section, 'fetch').split('\n'): dst = os.path.join(download_directory, os.path.basename(src)) fetch_url(src, dst) print('') return modules # ------------------------------------------------------------------------------ def main(argv): """ Main function. """ if len(argv) != 2: sys.stderr.write("\nYou can update a project in two steps.\n\n") sys.stderr.write("Step 1: Update or create infrastructure files\n") sys.stderr.write(" which will be needed to configure and build the project:\n") sys.stderr.write(" $ %s --self\n\n" % argv[0]) sys.stderr.write("Step 2: Create CMakeLists.txt and setup.py in PROJECT_ROOT:\n") sys.stderr.write(" $ %s \n" % argv[0]) sys.stderr.write(" example:\n") sys.stderr.write(" $ %s ..\n" % argv[0]) sys.exit(-1) if argv[1] == '--self': # update self if not os.path.isfile('autocmake.cfg'): print('- fetching example autocmake.cfg') fetch_url( src='%s/raw/master/example/autocmake.cfg' % AUTOCMAKE_GITHUB_URL, dst='autocmake.cfg' ) if not os.path.isfile('.gitignore'): print('- creating .gitignore') with open('.gitignore'), 'w') as f: f.write('*.pyc\n') print('- fetching lib/config.py') fetch_url( src='%s/raw/master/lib/config.py' % AUTOCMAKE_GITHUB_URL, dst='lib/config.py' ) print('- fetching lib/docopt.py') fetch_url( src='https://github.com/docopt/docopt/raw/master/docopt.py', dst='lib/docopt.py' ) print('- fetching update.py') fetch_url( src='%s/raw/master/update.py' % AUTOCMAKE_GITHUB_URL, dst='update.py' ) sys.exit(0) project_root = argv[1] if not os.path.isdir(project_root): sys.stderr.write("ERROR: %s is not a directory\n" % project_root) sys.exit(-1) # read config file print('- parsing autocmake.cfg') config = RawConfigParser(dict_type=OrderedDict) config.read('autocmake.cfg') # get relative path from setup.py script to this directory relative_path = os.path.relpath(os.path.abspath('.'), project_root) # fetch modules from the web or from relative paths modules = fetch_modules(config, relative_path) # create CMakeLists.txt print('- generating CMakeLists.txt') s = gen_cmakelists(config, relative_path, modules) with open(os.path.join(project_root, 'CMakeLists.txt'), 'w') as f: f.write('%s\n' % '\n'.join(s)) # create setup.py print('- generating setup.py') s = gen_setup(config, relative_path) file_path = os.path.join(project_root, 'setup.py') with open(file_path, 'w') as f: f.write('%s\n' % '\n'.join(s)) if sys.platform != 'win32': make_executable(file_path) # ------------------------------------------------------------------------------ # http://stackoverflow.com/a/30463972 def make_executable(path): mode = os.stat(path).st_mode mode |= (mode & 0o444) >> 2 # copy R bits to X os.chmod(path, mode) # ------------------------------------------------------------------------------ def parse_cmake_module(s_in): config_docopt = None config_define = None config_export = None config_fetch = None if 'autocmake.cfg configuration::' not in s_in: return config_docopt, config_define, config_export, config_fetch s_out = [] is_rst_line = False for line in s_in.split('\n'): if is_rst_line: if len(line) > 0: if line[0] != '#': is_rst_line = False else: is_rst_line = False if is_rst_line: s_out.append(line[2:]) if '#.rst:' in line: is_rst_line = True autocmake_entry = '\n'.join(s_out).split('autocmake.cfg configuration::')[1] autocmake_entry = autocmake_entry.replace('\n ', '\n') # we prepend a fake section heading so that we can parse it with configparser autocmake_entry = '[foo]\n' + autocmake_entry buf = StringIO(autocmake_entry) config = RawConfigParser(dict_type=OrderedDict) config.readfp(buf) for section in config.sections(): if config.has_option(section, 'docopt'): config_docopt = config.get(section, 'docopt') if config.has_option(section, 'define'): config_define = config.get(section, 'define') if config.has_option(section, 'export'): config_export = config.get(section, 'export') if config.has_option(section, 'fetch'): config_fetch = config.get(section, 'fetch') return config_docopt, config_define, config_export, config_fetch # ------------------------------------------------------------------------------ def test_parse_cmake_module(): s = '''#.rst: # # Foo ... # # autocmake.cfg configuration:: # # docopt: --cxx= C++ compiler [default: g++]. # --extra-cxx-flags= Extra C++ compiler flags [default: '']. # export: 'CXX=%s' % arguments['--cxx'] # define: '-DEXTRA_CXXFLAGS="%s"' % arguments['--extra-cxx-flags'] enable_language(CXX) if(NOT DEFINED CMAKE_C_COMPILER_ID) message(FATAL_ERROR "CMAKE_C_COMPILER_ID variable is not defined!") endif()''' config_docopt, config_define, config_export, config_fetch = parse_cmake_module(s) assert config_docopt == "--cxx= C++ compiler [default: g++].\n--extra-cxx-flags= Extra C++ compiler flags [default: '']." s = '''#.rst: # # Foo ... # # Bar ... enable_language(CXX) if(NOT DEFINED CMAKE_C_COMPILER_ID) message(FATAL_ERROR "CMAKE_C_COMPILER_ID variable is not defined!") endif()''' config_docopt, config_define, config_export, config_fetch = parse_cmake_module(s) assert config_docopt is None # ------------------------------------------------------------------------------ if __name__ == '__main__': main(sys.argv)