Skip to content
Snippets Groups Projects
Commit b908185d authored by Tomáš Pecka's avatar Tomáš Pecka Committed by Jan Trávníček
Browse files

Build: Config proposal

parent 6b9a4eb8
No related branches found
No related tags found
No related merge requests found
Showing
with 313 additions and 232 deletions
[Versioning]
major = 0
minor = 0
patch = 1
[CMake:Templates]
root = CMakeLists_root.txt
lib = CMakeLists_lib.txt
bin = CMakeLists_bin.txt
[CMake:Sources]
ExcludeSources: .txx, .cxx, .hxx
SourcesDir: src
TestSourcesDir: test-src
[ModulesLib]
alib2algo
alib2algo_experimental
alib2aux
alib2cli
alib2common
alib2data
alib2data_experimental
alib2elgo
alib2measure
alib2raw
alib2raw_cli_integration
alib2std
alib2str
alib2str_cli_integration
alib2xml
alib2abstraction
alib2dummy
alib2graph_data
alib2graph_algo
alib2gui
[ModulesBin]
aaccess2
aarbology2
acast2
acompaction2
acompare2
aconversions2
aconvert2
aderivation2
adeterminize2
aecho2
aepsilon2
agenerate2
aintegral2
aintrospection2
alangop2
aminimize2
anormalize2
aql2
aquery2
arand2
araw2
arename2
areverse2
arun2
astat2
astringology2
atrim2
tniceprint
agui2
[Versioning]
; Promote version with release
major = 0
minor = 0
patch = 1
[General]
ProjectConfFile: project.conf
PathToSources: ..
[CMake:Categories]
; projects and their templates
; project must belong in one of these categories
root: CMakeLists_root.txt
executable: CMakeLists_bin.txt
library: CMakeLists_lib.txt
[CMake:Sources]
; where to find sources, relative to {PROJECT_DIR}
ExcludeSources: .txx .cxx .hxx
SourcesDir: src
TestSourcesDir: test-src
; ----------------------------------------------------------
; system dependencies:
; if projects depends on one of these probably want to specify what should be passed inside target_* in cmake
[CMake:Deps:xml2]
include: PUBLIC ${LIBXML2_INCLUDE_DIR}
link: ${LIBXML2_LIBRARIES}
[CMake:Deps:threads]
link: ${CMAKE_THREAD_LIBS_INIT}
[CMake:Deps:Qt5Widgets]
link: Qt5::Widgets
include: ${Qt5Widgets_INCLUDE_DIRS}
[CMake:Deps:Qt5Xml]
link: Qt5::Xml
include:${Qt5Xml_INCLUDE_DIRS}
[CMake:Deps:graphviz]
link: ${GRAPHVIZ_GVC_LIBRARY} ${GRAPHVIZ_CGRAPH_LIBRARY}
include: ${GRAPHVIZ_INCLUDE_DIR}
[CMake:Deps:json]
link: ${JSONCPP_LIBRARIES}
include: ${jsoncpp_INCLUDE_DIRS}
[CMake:Deps:readline]
link: ${Readline_LIBRARY}
include: ${Readline_INCLUDE_DIR}
\ No newline at end of file
...@@ -5,8 +5,8 @@ import sys ...@@ -5,8 +5,8 @@ import sys
import configparser import configparser
import argparse import argparse
   
# ----------------------------------------------------------------------------------------------------------------------
   
# ----------------------------------------------------------------------------------------------------------------------
   
class SmartOpen: class SmartOpen:
SEPARATOR_SIZE = 20 SEPARATOR_SIZE = 20
...@@ -15,11 +15,7 @@ class SmartOpen: ...@@ -15,11 +15,7 @@ class SmartOpen:
self._dry_run = dry_run self._dry_run = dry_run
self._filename = filename self._filename = filename
self._mode = mode self._mode = mode
self._fd = sys.stdout if self._dry_run else open(os.path.join(directory, filename), mode=mode, encoding=encoding)
if self._dry_run:
self._fd = sys.stdout
else:
self._fd = open(os.path.join(directory, filename), mode=mode, encoding=encoding)
   
def read(self): def read(self):
return self._fd.read() return self._fd.read()
...@@ -37,179 +33,252 @@ class SmartOpen: ...@@ -37,179 +33,252 @@ class SmartOpen:
if self._fd != sys.stdout: if self._fd != sys.stdout:
self._fd.close() 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))
   
SCRIPT_DIRECTORY = os.path.join(os.path.dirname(__file__)) @staticmethod
ALIB_DIRECTORY = os.path.join(SCRIPT_DIRECTORY, "..") 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))]
   
config = configparser.ConfigParser(allow_no_value=True)
config.read(os.path.join(SCRIPT_DIRECTORY, 'config.ini'))
   
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
   
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))
   
PACKAGES = {
'lib': {key for key in config['ModulesLib']},
'bin': {key for key in config['ModulesBin']},
}
   
TEMPLATES = { # ----------------------------------------------------------------------------------------------------------------------
tpl: SmartOpen(config['CMake:Templates'][tpl], directory=SCRIPT_DIRECTORY, mode='r').read() for tpl in ['root', 'lib', 'bin']
}
   
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))
   
def to_rows(lst, sep='\n '): @property
return sep.join(lst) 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=[])
   
def get_source_files(project_name, directory): @property
# Recursively find all source file(s) and directories def system_deps_test(self):
source_files = [] return ConfigHelpers.c_get_array(self._config, 'Dependencies', 'system', fallback=[])
relpath_ref = os.path.join(ALIB_DIRECTORY, project_name)
   
exclude_sources = tuple(map(lambda s: s.strip(), config['CMake:Sources']['ExcludeSources'].split(','))) def find_sources(self):
return self._find_sources(CONFIG['CMake:Sources']['SourcesDir'])
   
for dp, dn, fn in os.walk(os.path.join(relpath_ref, directory)): def find_sources_test(self):
source_files = source_files + [os.path.relpath(os.path.join(dp, file), relpath_ref) return self._find_sources(CONFIG['CMake:Sources']['TestSourcesDir'])
for file in fn if not file.endswith(exclude_sources)]
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
   
return source_files
   
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
   
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
   
def parse_makeconfig(file):
link_libs = []
link_test_libs = []
include_libs = []
include_test_libs = []
cmake_options = []
# Read and parse makefile.conf
with open(file, 'r') as file:
for line in file:
if line.startswith('LINK_LIBRARIES'):
link_libs = line[len('LINK_LIBRARIES='):].split()
elif line.startswith('SYSTEM_LIBRARIES'):
sys_libs = line[len('SYSTEM_LIBRARIES='):].split()
for lib in sys_libs:
if lib == 'xml2':
link_libs.append('${LIBXML2_LIBRARIES}')
include_libs.append('PUBLIC ${LIBXML2_INCLUDE_DIR}')
elif lib == 'threads':
link_libs.append('${CMAKE_THREAD_LIBS_INIT}')
elif lib == 'Qt5Widgets':
link_libs.append('Qt5::Widgets')
include_libs.append('${Qt5Widgets_INCLUDE_DIRS}')
elif lib == 'Qt5Xml':
link_libs.append('Qt5::Xml')
include_libs.append('${Qt5Xml_INCLUDE_DIRS}')
elif lib == 'graphviz':
link_libs.append('${GRAPHVIZ_GVC_LIBRARY}')
link_libs.append('${GRAPHVIZ_CGRAPH_LIBRARY}')
include_libs.append('${GRAPHVIZ_INCLUDE_DIR}')
elif lib == 'json':
link_libs.append('${JSONCPP_LIBRARIES}')
include_libs.append('${jsoncpp_INCLUDE_DIRS}')
else:
link_libs.append(lib)
elif line.startswith('TEST_LINK_LIBRARIES'):
link_test_libs = line[len('TEST_LINK_LIBRARIES='):].split()
elif line.startswith('TEST_SYSTEM_LIBRARIES'):
sys_libs = line[len('TEST_SYSTEM_LIBRARIES='):].split()
for lib in sys_libs:
link_test_libs.append()
elif line.startswith('AUTOMOC=ON'):
cmake_options.append('set(CMAKE_AUTOMOC ON)')
elif line.startswith('AUTOUIC=ON'):
cmake_options.append('set(CMAKE_AUTOUIC ON)')
return ' '.join(link_libs), ' '.join(link_test_libs), ' '.join(include_libs), ' '.join(
include_test_libs), '\n'.join(cmake_options)
   
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
   
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 create_root_cmakelist(dry_run, packages):
with SmartOpen("CMakeLists.txt", 'w', directory=ALIB_DIRECTORY, dry_run=dry_run) as f:
f.write(TEMPLATES['root'].format(
alib_modules_lib=to_rows(packages['library']),
alib_modules_exe=to_rows(packages['executable']),
alib_versioning_major=config['Versioning']['major'],
alib_versioning_minor=config['Versioning']['minor'],
alib_versioning_patch=config['Versioning']['patch'],
))
def create_library_package(project_name, dry_run):
link_libs, link_test_libs, include_libs, include_test_libs, cmake_options = parse_makeconfig(
os.path.join(ALIB_DIRECTORY, project_name, 'makefile.conf'))
with SmartOpen("CMakeLists.txt", 'w', directory=os.path.join(ALIB_DIRECTORY, project_name), dry_run=dry_run) as f:
f.write(TEMPLATES['lib'].format(
project_name=project_name,
target_libs=link_libs,
target_test_libs=link_test_libs,
include_paths=include_libs,
include_test_paths=include_test_libs,
cmake_options=cmake_options,
source_files=to_rows(get_source_files(project_name, config['CMake:Sources']['SourcesDir'])),
source_files_test=to_rows(get_source_files(project_name, config['CMake:Sources']['TestSourcesDir'])),
))
def create_executable_package(project_name, dry_run):
link_libs, link_test_libs, include_libs, include_test_libs, cmake_options = parse_makeconfig(
os.path.join(ALIB_DIRECTORY, project_name, 'makefile.conf'))
with SmartOpen("CMakeLists.txt", 'w', directory=os.path.join(ALIB_DIRECTORY, project_name), dry_run=dry_run) as f:
f.write(TEMPLATES['bin'].format(
project_name=project_name,
target_libs=link_libs,
target_test_libs=link_test_libs,
include_paths=include_libs,
include_test_paths=include_test_libs,
cmake_options=cmake_options,
source_files=to_rows(get_source_files(project_name, config['CMake:Sources']['SourcesDir'])),
))
   
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
   
   
def main(dry_run, main_file, packages): def main(dry_run, main_file, packages_requested, no_dependencies):
packages = { if packages_requested is None:
'library': set(packages) & PACKAGES['lib'], packages = PROJECTS.values()
'executable': set(packages) & PACKAGES['bin'], 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_functions = { packages = [PROJECTS[p] for p in set(packages)]
'library': create_library_package,
'executable': create_executable_package,
}
   
packages_cnt = sum([len(v) for k, v in packages.items()]) + (main_file is True) print("The following packages will be generated:", file=sys.stderr)
print(", ".join(sorted([p.name for p in packages])), file=sys.stderr)
   
i = 1 packages_cnt = len(packages) + (main_file is True)
for type, packages_set in packages.items():
for package in packages_set:
try:
packages_functions[type](package, dry_run)
print('[{}/{}] Generated {} package {}'.format(i, packages_cnt, type, package), file=sys.stderr)
except FileNotFoundError as e:
print('[{}/{}] Skipping library package {}: {}'.format(i, packages_cnt, type, package, e), file=sys.stderr)
   
i += 1 # 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 # Generate root file
if main_file: if main_file:
create_root_cmakelist(dry_run, packages) Generator.generate_root(None, dry_run)
print('[{}/{}] Generated main CMakeLists.txt'.format(i, packages_cnt), file=sys.stderr) print('[{}/{}] Generated main CMakeLists.txt'.format(packages_cnt, packages_cnt), file=sys.stderr)
return 0
   
# ---------------------------------------------------------------------------------------------------------------------- # ----------------------------------------------------------------------------------------------------------------------
   
...@@ -219,10 +288,7 @@ if __name__ == '__main__': ...@@ -219,10 +288,7 @@ if __name__ == '__main__':
parser.add_argument('-w', '--write', action='store_true', default=False, help="Write files") 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('-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('-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() args = parser.parse_args()
sys.exit(main(not args.write, args.main, args.packages, args.no_deps))
if args.packages is None:
args.packages = list(PACKAGES['lib'] | PACKAGES['bin'])
main(not args.write, args.main, args.packages)
LINK_LIBRARIES=alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
SYSTEM_LIBRARIES=xml2
[General]
category: executable
[Dependencies]
project: alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
system: xml2
LINK_LIBRARIES=alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
SYSTEM_LIBRARIES=xml2
[General]
category: executable
[Dependencies]
project: alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
system: xml2
LINK_LIBRARIES=alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
SYSTEM_LIBRARIES=xml2
[General]
category: executable
[Dependencies]
project: alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
system: xml2
LINK_LIBRARIES=alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
SYSTEM_LIBRARIES=xml2
[General]
category: executable
[Dependencies]
project: alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
system: xml2
LINK_LIBRARIES=alib2cli alib2aux alib2str alib2graph_data alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
SYSTEM_LIBRARIES=xml2
[General]
category: executable
[Dependencies]
project: alib2cli alib2aux alib2str alib2graph_data alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
system: xml2
LINK_LIBRARIES=alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
SYSTEM_LIBRARIES=xml2
[General]
category: executable
[Dependencies]
project: alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
system: xml2
LINK_LIBRARIES=alib2cli alib2aux alib2str alib2graph_data alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
SYSTEM_LIBRARIES=xml2
[General]
category: executable
[Dependencies]
project: alib2cli alib2aux alib2str alib2graph_data alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
system: xml2
LINK_LIBRARIES=alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
SYSTEM_LIBRARIES=xml2
[General]
category: executable
[Dependencies]
project: alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
system: xml2
LINK_LIBRARIES=alib2cli alib2elgo alib2algo alib2str alib2data alib2xml alib2common alib2abstraction alib2measure alib2std
SYSTEM_LIBRARIES=xml2
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment