Skip to content
Snippets Groups Projects
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))