#!/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))