Code owners
Assign users and groups as approvers for specific file changes. Learn more.
generate.py 11.57 KiB
#!/usr/bin/env python3
import os
import sys
import configparser
import argparse
# ----------------------------------------------------------------------------------------------------------------------
class SmartOpen:
SEPARATOR_SIZE = 20
def __init__(self, filename, mode='r', *, directory='.', encoding=None, dry_run=False):
self._dry_run = dry_run
self._filename = filename
self._mode = mode
self._fd = sys.stdout if self._dry_run else open(os.path.join(directory, filename), mode=mode, encoding=encoding)
def read(self):
return self._fd.read()
def __enter__(self):
if self._dry_run and 'w' in self._mode:
self._fd.write('{}\n'.format(self._filename) + '-' * self.SEPARATOR_SIZE + '\n')
return self._fd
def __exit__(self, d_type, value, traceback):
if self._dry_run and 'w' in self._mode:
self._fd.write('-' * self.SEPARATOR_SIZE + '\n')
if self._fd != sys.stdout:
self._fd.close()
# ----------------------------------------------------------------------------------------------------------------------
class Helpers:
@staticmethod
def join(lst, sep='\n '):
return sep.join(lst)
@staticmethod
def path(*args):
return os.path.realpath(os.path.join(*args))
@staticmethod
def find_dirs_with_file(directory, filename):
return [e.name for e in os.scandir(directory) if e.is_dir() and os.path.isfile(Helpers.path(e.path, filename))]
# ----------------------------------------------------------------------------------------------------------------------
class ConfigException(Exception):
pass
class ConfigHelpers:
@staticmethod
def c_get(c, section, key, fallback=None):
try:
return c.get(section, key)
except (configparser.NoOptionError, configparser.NoSectionError):
if fallback is not None:
return fallback
raise ConfigException("[{}][{}] not defined".format(section, key))
@staticmethod
def c_get_array(c, section, key, fallback=None):
try:
return c.get(section, key).strip().split()
except (configparser.NoOptionError, configparser.NoSectionError):
if fallback is not None:
return fallback
raise ConfigException("[{}][{}] not defined".format(section, key))
@staticmethod
def c_section(c, section, fallback=None):
try:
return {k: v for k, v in c[section].items()}
except (configparser.NoSectionError, KeyError):
if fallback is not None:
return fallback
raise ConfigException("[{}] not defined".format(section))
# ----------------------------------------------------------------------------------------------------------------------
class ProjectException(Exception):
pass
class Project:
def __init__(self, name, conf):
self.name = name
self.path = Helpers.path(ALIB_DIRECTORY, self.name)
self._config = configparser.ConfigParser(allow_no_value=True)
self._config.read(Helpers.path(self.path, conf))
@property
def category(self):
ctg = ConfigHelpers.c_get(self._config, 'General', 'category')
if ctg not in CATEGORIES.keys():
raise ProjectException("Invalid category ({})".format(ctg))
return ctg
@property
def cmake_additional_set(self):
return ConfigHelpers.c_section(self._config, 'CMake', fallback={}).items()
@property
def dependencies(self):
return ConfigHelpers.c_get_array(self._config, 'Dependencies', 'project', fallback=[])
@property
def dependencies_test(self):
return ConfigHelpers.c_get_array(self._config, 'TestDependencies', 'project', fallback=[])
@property
def system_deps(self):
return ConfigHelpers.c_get_array(self._config, 'Dependencies', 'system', fallback=[])
@property
def system_deps_test(self):
return ConfigHelpers.c_get_array(self._config, 'Dependencies', 'system', fallback=[])
def find_sources(self):
return self._find_sources(CONFIG['CMake:Sources']['SourcesDir'])
def find_sources_test(self):
return self._find_sources(CONFIG['CMake:Sources']['TestSourcesDir'])
def _find_sources(self, directory, exclude_sources=None):
# Recursively find all source file(s) and directories, return their names relatively to alib/{project}/
if exclude_sources is None:
exclude_sources = tuple(map(lambda s: s.strip(), CONFIG['CMake:Sources']['ExcludeSources'].split(' ')))
source_files = []
for dp, dn, fn in os.walk(Helpers.path(self.path, directory)):
source_files = source_files + [os.path.relpath(Helpers.path(dp, file), self.path)
for file in fn if not file.endswith(exclude_sources)]
return source_files
def generate(self, dry_run):
return Generator().generate(self, dry_run)
def recursive_dependencies(self):
visited = {self.name}
stack = [self.name]
while len(stack) > 0:
c = stack.pop()
for dep in PROJECTS[c].dependencies:
if dep not in visited:
visited.add(dep)
stack.append(dep)
return visited
# ----------------------------------------------------------------------------------------------------------------------
class Generator:
@classmethod
def generate(cls, project, dry_run):
foo = getattr(cls, 'generate_{}'.format(project.category))
return foo(project, dry_run)
@classmethod
def generate_root(cls, project, dry_run):
with SmartOpen("CMakeLists.txt", 'w', directory=ALIB_DIRECTORY, dry_run=dry_run) as f:
f.write(CATEGORIES['root'].format(
alib_modules_lib=Helpers.join([p.name for p in PROJECTS.values() if p.category == "library"]),
alib_modules_exe=Helpers.join([p.name for p in PROJECTS.values() if p.category == "executable"]),
alib_versioning_major=CONFIG['Versioning']['major'],
alib_versioning_minor=CONFIG['Versioning']['minor'],
alib_versioning_patch=CONFIG['Versioning']['patch'],
))
@classmethod
def generate_library(cls, project, dry_run):
sys_includes, sys_libs = cls._handle_system_deps(project.system_deps)
test_sys_includes, test_sys_libs = cls._handle_system_deps(project.system_deps)
with SmartOpen("CMakeLists.txt", 'w', directory=project.path, dry_run=dry_run) as f:
f.write(CATEGORIES[project.category].format(
project_name=project.name,
target_libs=Helpers.join(project.dependencies + sys_libs, ' '),
target_test_libs=Helpers.join(project.dependencies_test + test_sys_libs, ' '),
include_paths=Helpers.join(sys_includes, ' '),
include_test_paths=Helpers.join(test_sys_includes, ' '),
cmake_options=Helpers.join(["set({} {})".format(k.upper(), v) for k, v in project.cmake_additional_set], sep='\n'),
source_files=Helpers.join(project.find_sources()),
source_files_test=Helpers.join(project.find_sources_test()),
))
@classmethod
def generate_executable(cls, project, dry_run):
sys_includes, sys_libs = cls._handle_system_deps(project.system_deps)
test_sys_includes, test_sys_libs = cls._handle_system_deps(project.system_deps)
with SmartOpen("CMakeLists.txt", 'w', directory=project.path, dry_run=dry_run) as f:
f.write(CATEGORIES[project.category].format(
project_name=project.name,
target_libs=Helpers.join(project.dependencies + sys_libs, ' '),
target_test_libs=Helpers.join(project.dependencies_test + test_sys_libs, ' '),
include_paths=Helpers.join([project.name] + sys_includes, ' '),
include_test_paths=Helpers.join([project.name] + test_sys_includes, ' '),
cmake_options=project.cmake_additional_set,
source_files=Helpers.join(project.find_sources()),
))
@staticmethod
def _handle_system_deps(system_deps):
libs, incl = [], []
for dep in system_deps:
incl = incl + ConfigHelpers.c_get_array(CONFIG, 'CMake:Deps:{}'.format(dep), 'include', fallback=[])
libs = libs + ConfigHelpers.c_get_array(CONFIG, 'CMake:Deps:{}'.format(dep), 'link', fallback=[])
return incl, libs
# ----------------------------------------------------------------------------------------------------------------------
SCRIPT_DIRECTORY = Helpers.path(os.path.dirname(__file__))
CONFIG = configparser.ConfigParser(allow_no_value=True)
CONFIG.read(Helpers.path(SCRIPT_DIRECTORY, 'generate.conf'))
ALIB_DIRECTORY = Helpers.path(SCRIPT_DIRECTORY, CONFIG['General']['PathToSources'])
PROJECTS = {
project: Project(project, CONFIG['General']['ProjectConfFile'])
for project in Helpers.find_dirs_with_file(ALIB_DIRECTORY, CONFIG['General']['ProjectConfFile'])
}
CATEGORIES = {
category: SmartOpen(template, directory=SCRIPT_DIRECTORY, mode='r').read()
for category, template in CONFIG['CMake:Categories'].items()
}
# ----------------------------------------------------------------------------------------------------------------------
def main(dry_run, main_file, packages_requested, no_dependencies):
if packages_requested is None:
packages = PROJECTS.values()
else:
# Filter out invalid packages and possibly compute dependencies
packages = set()
for p in packages_requested:
if p not in PROJECTS:
print("Warning: Package {} is not a valid project. Skipping".format(p), file=sys.stderr)
continue
if no_dependencies:
packages.add(p)
else:
for dep in PROJECTS[p].recursive_dependencies():
packages.add(dep)
packages = [PROJECTS[p] for p in set(packages)]
print("The following packages will be generated:", file=sys.stderr)
print(", ".join(sorted([p.name for p in packages])), file=sys.stderr)
packages_cnt = len(packages) + (main_file is True)
# Generate packages
for i, p in enumerate(packages, 1):
try:
p.generate(dry_run)
print('[{}/{}] Generated {} package {}'.format(i, packages_cnt, p.category, p.name), file=sys.stderr)
except ProjectException as e:
print('[{}/{}] Error while generating {}: {}'.format(i, packages_cnt, p.name, e), file=sys.stderr)
return 1
# Generate root file
if main_file:
Generator.generate_root(None, dry_run)
print('[{}/{}] Generated main CMakeLists.txt'.format(packages_cnt, packages_cnt), file=sys.stderr)
return 0
# ----------------------------------------------------------------------------------------------------------------------
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='CMakeLists generator for Algorithms Library Toolkit')
parser.add_argument('-w', '--write', action='store_true', default=False, help="Write files")
parser.add_argument('-m', '--main', action='store_true', default=False, help="Generate main CMakeLists.txt")
parser.add_argument('-p', '--packages', action='append', help='Specify packages to build. For multiple packages, use -p multiple times')
parser.add_argument('--no-deps', action='store_true', default=False, help="Don't generate dependencies")
args = parser.parse_args()
sys.exit(main(not args.write, args.main, args.packages, args.no_deps))