autocmake/update.py
2015-08-27 17:41:20 +02:00

484 lines
16 KiB
Python

#!/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('%s' % arguments['--cmake-executable'])")
# 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] [<builddir>]')
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=<TYPE>', 'Set the CMake build type (debug, release, or relwithdeb) [default: release].'])
options.append(['--generator=<STRING>', 'Set the CMake build system generator [default: Unix Makefiles].'])
options.append(['--show', 'Show CMake command and exit.'])
options.append(['--cmake-executable=<CMAKE_EXECUTABLE>', 'Set the CMake executable [default: cmake].'])
options.append(['--cmake-options=<OPTIONS>', 'Define options to CMake [default: None].'])
options.append(['<builddir>', '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['<builddir>']")
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 <PROJECT_ROOT>\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=<CXX> C++ compiler [default: g++].
# --extra-cxx-flags=<EXTRA_CXXFLAGS> 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=<CXX> C++ compiler [default: g++].\n--extra-cxx-flags=<EXTRA_CXXFLAGS> 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)