autocmake/update.py
2022-10-24 15:57:34 +02:00

304 lines
11 KiB
Python

#!/usr/bin/env python3
import os
import sys
AUTOCMAKE_GITHUB_URL = 'https://github.com/AnotherFoxGuy/autocmake/raw/master/'
def licensing_info():
return '''The CMake infrastructure for this project is generated using [Autocmake]
by Radovan Bast, Roberto Di Remigio, Jonas Juselius and contributors.
The `update.py` Python script and the contents of the directories `autocmake` and `downloaded` are licensed
under the terms of the [BSD-3-Clause license], unless otherwise stated.
[Autocmake]: http://autocmake.org
[BSD-3-Clause license]: https://tldrlegal.com/license/bsd-3-clause-license-(revised)'''
def check_for_yaml():
try:
import yaml
except ImportError:
sys.stderr.write("ERROR: you need to install the pyyaml package\n")
sys.exit(-1)
def print_progress_bar(text, done, total, width):
"""
Print progress bar.
"""
if total > 0:
n = int(float(width) * float(done) / float(total))
sys.stdout.write("\r{0} [{1}{2}] ({3}/{4})".format(text, '#' * n, ' ' * (width - n), done, total))
sys.stdout.flush()
def flat_add(lst, x):
if isinstance(x, int):
lst.append(x)
return lst
elif isinstance(x, str):
lst.append(x)
return lst
else:
return lst + x
def fetch_modules(config, relative_path, download_directory):
"""
Assemble modules which will
be included in CMakeLists.txt.
"""
from collections import namedtuple, defaultdict
from autocmake.extract import extract_list, to_d, to_l
from autocmake.parse_rst import parse_cmake_module
cleaned_config = defaultdict(lambda: [])
modules = []
Module = namedtuple('Module', 'path name')
num_sources = len(extract_list(config, 'source'))
print_progress_bar(text='- assembling modules:',
done=0,
total=num_sources,
width=30)
if 'modules' in config:
i = 0
for t in config['modules']:
for k, v in t.items():
d = to_d(v)
for _k, _v in to_d(v).items():
cleaned_config[_k] = flat_add(cleaned_config[_k], _v)
# fetch sources and parse them
if 'source' in d:
for src in to_l(d['source']):
i += 1
# we download the file
module_name = os.path.basename(src)
if 'http' in src:
path = download_directory
name = 'autocmake_{0}'.format(module_name)
dst = os.path.join(download_directory, 'autocmake_{0}'.format(module_name))
fetch_url(src, dst)
file_name = dst
fetch_dst_directory = download_directory
else:
if os.path.exists(src):
path = os.path.dirname(src)
name = module_name
file_name = src
fetch_dst_directory = path
else:
sys.stderr.write("ERROR: {0} does not exist\n".format(src))
sys.exit(-1)
# we infer config from the module documentation
# dictionary d overrides the configuration in the module documentation
# this allows to override interpolation inside the module
with open(file_name, 'r') as f:
parsed_config = parse_cmake_module(f.read(), d)
for _k2, _v2 in parsed_config.items():
if _k2 not in to_d(v):
# we add to clean_config only if the entry does not exist
# in parent autocmake.yml already
# this allows to override
cleaned_config[_k2] = flat_add(cleaned_config[_k2], _v2)
modules.append(Module(path=path, name=name))
print_progress_bar(text='- assembling modules:',
done=i,
total=num_sources,
width=30)
print('')
return modules, cleaned_config
def process_yaml(argv):
from autocmake.parse_yaml import parse_yaml
from autocmake.generate import gen_cmakelists, gen_setup
project_root = argv[1]
if not os.path.isdir(project_root):
sys.stderr.write("ERROR: {0} is not a directory\n".format(project_root))
sys.exit(-1)
# read config file
print('- parsing autocmake.yml')
with open('autocmake.yml', 'r') as stream:
config = parse_yaml(stream)
if 'name' in config:
project_name = config['name']
else:
sys.stderr.write("ERROR: you have to specify the project name in autocmake.yml\n")
sys.exit(-1)
if ' ' in project_name.rstrip():
sys.stderr.write("ERROR: project name contains a space\n")
sys.exit(-1)
if 'language' in config:
project_language = ' '.join(config['language']) if isinstance(config['language'], list) else config['language']
else:
sys.stderr.write("ERROR: you have to specify the project language(s) in autocmake.yml\n\n")
sys.stderr.write("# for instance like this (several languages):\nlanguage:\n - CXX\n - Fortran\n\n")
sys.stderr.write("# or like this (one language):\nlanguage: Fortran\n\n")
sys.exit(-1)
if 'min_cmake_version' in config:
min_cmake_version = config['min_cmake_version']
else:
sys.stderr.write("ERROR: you have to specify min_cmake_version in autocmake.yml\n")
sys.exit(-1)
if 'default_build_type' in config:
default_build_type = config['default_build_type'].lower()
else:
sys.stderr.write("ERROR: you have to specify default_build_type in autocmake.yml\n\n")
sys.stderr.write("# for instance like this (debug, release, relwithdebinfo, or minsizerel):\ndefault_build_type: release\n\n")
sys.exit(-1)
if 'setup_script' in config:
setup_script_name = config['setup_script']
else:
setup_script_name = 'setup'
# get relative path from setup script to this directory
relative_path = os.path.relpath(os.path.abspath('.'), project_root)
download_directory = 'downloaded'
if not os.path.exists(download_directory):
os.makedirs(download_directory)
# fetch modules from the web or from relative paths
modules, cleaned_config = fetch_modules(config, relative_path, download_directory)
# fetch files which are not parsed
for src in cleaned_config['fetch']:
dst = os.path.join(download_directory, os.path.basename(src))
fetch_url(src, dst)
# print warnings
for warning in cleaned_config['warning']:
print('- WARNING: {0}'.format(warning))
# create CMakeLists.txt
print('- generating CMakeLists.txt')
s = gen_cmakelists(project_name, project_language, min_cmake_version, default_build_type, relative_path, modules)
with open(os.path.join(project_root, 'CMakeLists.txt'), 'w') as f:
f.write('{0}\n'.format('\n'.join(s)))
# create setup script unless it is 'None' or 'none'
if setup_script_name.lower() != 'none':
print('- generating setup script')
s = gen_setup(cleaned_config, default_build_type, relative_path, setup_script_name)
file_path = os.path.join(project_root, setup_script_name)
with open(file_path, 'w') as f:
f.write('{0}\n'.format('\n'.join(s)))
if sys.platform != 'win32':
make_executable(file_path)
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(" $ {0} --self\n\n".format(argv[0]))
sys.stderr.write("Step 2: Create CMakeLists.txt and setup script in PROJECT_ROOT:\n")
sys.stderr.write(" $ {0} <PROJECT_ROOT>\n".format(argv[0]))
sys.stderr.write(" example:\n")
sys.stderr.write(" $ {0} ..\n".format(argv[0]))
sys.exit(-1)
if argv[1] in ['-h', '--help']:
print('Usage:')
for t, h in [('python update.py --self',
'Update this script and fetch or update infrastructure files under autocmake/.'),
('python update.py <builddir>',
'(Re)generate CMakeLists.txt and setup script and fetch or update CMake modules.'),
('python update.py (-h | --help)',
'Show this help text.')]:
print(' {0:30} {1}'.format(t, h))
sys.exit(0)
if argv[1] == '--self':
# update self
if not os.path.isfile('autocmake.yml'):
print('- fetching example autocmake.yml')
fetch_url(
src='{0}example/autocmake.yml'.format(AUTOCMAKE_GITHUB_URL),
dst='autocmake.yml'
)
if not os.path.isfile('.gitignore'):
print('- creating .gitignore')
with open('.gitignore', 'w') as f:
f.write('*.pyc\n')
for f in ['autocmake/configure.py',
'autocmake/__init__.py',
'autocmake/external/docopt.py',
'autocmake/external/__init__.py',
'autocmake/generate.py',
'autocmake/extract.py',
'autocmake/interpolate.py',
'autocmake/parse_rst.py',
'autocmake/parse_yaml.py',
'update.py']:
print('- fetching {0}'.format(f))
fetch_url(
src='{0}{1}'.format(AUTOCMAKE_GITHUB_URL, f),
dst='{0}'.format(f)
)
# finally create a README.md with licensing information
with open('README.md', 'w') as f:
print('- generating licensing information')
f.write(licensing_info())
sys.exit(0)
process_yaml(argv)
def make_executable(path):
# http://stackoverflow.com/a/30463972
mode = os.stat(path).st_mode
mode |= (mode & 0o444) >> 2 # copy R bits to X
os.chmod(path, mode)
def fetch_url(src, dst):
"""
Fetch file from URL src and save it to dst.
"""
import urllib.request
import shutil
dirname = os.path.dirname(dst)
if dirname != '':
if not os.path.isdir(dirname):
os.makedirs(dirname)
try:
with urllib.request.urlopen(src) as response, open(dst, 'wb') as out_file:
shutil.copyfileobj(response, out_file)
except urllib.error.HTTPError as e:
sys.stderr.write(f"\n\nERROR: could not fetch {src}, request failed with error {e.code} \n\t {e.reason}\n")
sys.exit(-1)
except urllib.error.URLError as e:
sys.stderr.write(f"\n\nERROR: could not fetch {src}, {e.reason}\n")
sys.exit(-1)
if __name__ == '__main__':
check_for_yaml()
main(sys.argv)