diff --git a/DependencyGenerator.py b/DependencyGenerator.py index d5b73ee..6e7303e 100644 --- a/DependencyGenerator.py +++ b/DependencyGenerator.py @@ -1,12 +1,27 @@ -import json, os, sys +import json, sys from urllib.parse import urlparse -tab = " "*4 -nl_indent = "\n"+tab*2 +tab = " " * 4 +nl_indent = "\n" + tab * 2 -def escape(s, n): - return ' ' * n + s +def target_relative_path(path, name=None, src_dir=None, bin_dir=None): + if src_dir is None: + src_dir = "${" + name + "_SOURCE_DIR}" + if bin_dir is None: + bin_dir = "${CMAKE_CURRENT_BINARY_DIR}/" + name + + if path.startswith('/'): + return '${PROJECT_SOURCE_DIR}' + path + elif path.startswith(':'): + return bin_dir + '/' + path[1:] + elif path.startswith('${'): + return path + else: + if path == '.' or path == '': + return src_dir + else: + return src_dir + '/' + path def generate_dependency_decl(name, keys): @@ -14,19 +29,107 @@ def generate_dependency_decl(name, keys): for (key, value) in keys.items(): max_key_length = max(max_key_length, len(key)) + def align(s, n): + return ' ' * n + s + s = "import_dependency(" + name + nl_indent + (nl_indent).join( - map(lambda k: k[0] + " " + escape(k[1], max_key_length - len(k[0]) + 3), keys.items())) + ")" + map(lambda k: k[0] + " " + align(k[1], max_key_length - len(k[0]) + 3), keys.items())) + ")" return s +def to_cmake_datatype(value): + def escape_str(s): + if ' ' in s: + return '"' + s.replace('"', '\\"') + '"' + return s + + if isinstance(value, str): + return escape_str(value) + elif isinstance(value, bool): + return 'TRUE' if value else 'FALSE' + else: + return escape_str(str(value)) + + +def generate_target_definition(name, target_name, public_decl_type, src_dir, target_info): + def gen_entry(decl, values, normalizer=None): + if values is None or len(values) == 0: + return [] + if normalizer is None: + normalizer = lambda x: x + + if isinstance(values, str): + values = [values] + if isinstance(values, dict): + values = values.items() + + return list(map(lambda x: decl + " " + normalizer(x), values)) + + def gen_cmake_call(call, entries, glue=None): + if glue is None: + glue = '' + if len(entries) != 0: + print(call + "(" + target_name + glue + " " + nl_indent + + nl_indent.join(entries) + + ")") + + def gen_cmake_target_attrs(call, attr_name, default_scope=public_decl_type, normalizer=None): + values = [] + values += gen_entry(default_scope, target_info.get(attr_name), normalizer=normalizer) + values += gen_entry(public_decl_type, target_info.get("public_" + attr_name), normalizer=normalizer) + values += gen_entry('PRIVATE', target_info.get("private_" + attr_name), normalizer=normalizer) + values += gen_entry('INTERFACE', target_info.get("interface_" + attr_name), normalizer=normalizer) + gen_cmake_call(call, values) + + def gen_cmake_target_props(call, attr_name, glue=None): + if not attr_name in target_info: + return + + values = list(map(lambda x: x[0] + ' ' + to_cmake_datatype(x[1]), target_info[attr_name].items())) + gen_cmake_call(call, values, glue=glue) + + def cmake_define_kv(item): + if isinstance(item, str): + return item + if item[1] is None: + return item[0] + return item[0] + '=' + to_cmake_datatype(item[1]) + + # gen_cmake_target_attrs('target_sources', 'srcs', suffix=src_dir, default_scope='PRIVATE') + gen_cmake_target_attrs('target_include_directories', 'includes', + normalizer=lambda x: target_relative_path(x, name)) + gen_cmake_target_attrs('target_compile_definitions', 'defines', normalizer=cmake_define_kv) + gen_cmake_target_attrs('target_compile_options', 'options') + gen_cmake_target_attrs('target_compile_features', 'features') + gen_cmake_target_attrs('target_link_libraries', 'links') + gen_cmake_target_attrs('target_link_directories', 'link_directories') + gen_cmake_target_attrs('target_link_options', 'link_options') + gen_cmake_target_attrs('target_precompile_headers', 'precompile_headers') + gen_cmake_target_props('set_target_properties', 'properties', ' PROPERTIES') + + +def enumerate_srcs(name, target_name, srcs): + if isinstance(srcs, str): + srcs = [srcs] + target_name = target_name.replace('.', '_') + + globs = list(filter(lambda x: '*' in x, srcs)) + paths = list(filter(lambda x: not '*' in x, srcs)) + + paths = list(map(lambda x: target_relative_path(x, name), paths)) + if len(globs) != 0: + print("file(GLOB_RECURSE " + target_name + "_SRCS CONFIGURE_DEPENDS " + (nl_indent).join( + map(lambda x: target_relative_path(x, name), globs)) + ")") + paths = ["${" + target_name + "_SRCS}"] + paths + + return (nl_indent).join(paths) + + j = json.load(sys.stdin) for (name, dep_info) in j.items(): sys.stderr.write('Updating dependency definitions for ' + name + '\n') - target_info = dep_info["target"] - - target_type = target_info["type"] - src_dir = "${" + name + "_SOURCE_DIR}/" + src_dir = "${" + name + "_SOURCE_DIR}" dep_args = {} if "git" in dep_info: @@ -54,62 +157,95 @@ for (name, dep_info) in j.items(): print("# -- Dependency: " + name) print(generate_dependency_decl(name, dep_args)) - public_decl_type = "PUBLIC" - if target_type == "static": - if isinstance(target_info["srcs"], list): - srcs = (nl_indent).join(map(lambda x: src_dir + x,target_info["srcs"])) - else: - print("file(GLOB " + name + "_SRCS " + src_dir + target_info["srcs"] + ")") - srcs = "${" + name + "_SRCS}" - print("add_library("+name+" STATIC "+nl_indent+srcs+")") + if 'configure' in dep_info: + configures = dep_info['configure'] + for (conf_name, configure) in configures.items(): + configure_type = 'configure_file' + if 'type' in configure: + configure_type = configure['type'] - if target_type == "interface": - print("add_library("+name+" INTERFACE)") - public_decl_type = "INTERFACE" + if configure_type == 'configure': + print('configure_file(' + + nl_indent + to_cmake_datatype(target_relative_path(configure['input'], name)) + ' ' + + nl_indent + to_cmake_datatype(target_relative_path(conf_name, name)) + ')') + elif configure_type == 'generate': + print('file(GENERATE' + + (nl_indent + 'OUTPUT ' + to_cmake_datatype(target_relative_path(conf_name, name))) + + (nl_indent + 'CONTENT ' + to_cmake_datatype(configure['content']) if 'content' in configure else '') + + (nl_indent + 'INPUT ' + to_cmake_datatype((target_relative_path(configure['input'], name))) if 'input' in configure else '') + + (nl_indent + 'CONDITION ' + configure['condition'] if 'condition' in configure else '') + + ')') + else: + raise RuntimeError("Unsupported configure type!") - if target_type == "subdirectory": - if "cache" in target_info: - for (var_name, value) in target_info["cache"].items(): - if isinstance(value, bool): - print("set("+var_name+" "+("ON" if value else "OFF")+" CACHE INTERNAL \"\" FORCE)") + if len(configures) != 0: + print() - print("add_subdirectory(${" + name + "_SOURCE_DIR} ${" + name + "_BINARY_DIR})") + target_infos = dep_info["target"] + if isinstance(target_infos, dict): + target_infos = [target_infos] - if target_type == "cmake": - print("include(${PROJECT_SOURCE_DIR}/" + target_info["file"]+")\n") - continue + for target_n, target_info in enumerate(target_infos): + public_decl_type = "PUBLIC" - includes = [] - if "public_includes" in target_info: - includes += list(map(lambda x: public_decl_type + " " + src_dir + x, target_info["public_includes"])) - if "private_includes" in target_info: - includes += list(map(lambda x: "PRIVATE " + src_dir + x, target_info["private_includes"])) - if len(includes) != 0: - print("target_include_directories("+name+" "+nl_indent+ - nl_indent.join(includes) - +")") + target_name = name + if 'name' in target_info: + target_name = name + '.' + target_info['name'] + target_type = target_info["type"] - defines = [] - if "public_defines" in target_info: - defines += list(map(lambda x: public_decl_type + " " + x, target_info["public_defines"])) - if "private_defines" in target_info: - defines += list(map(lambda x: "PRIVATE " + x, target_info["private_defines"])) - if len(defines) != 0: - print("target_compile_definitions("+name+" "+nl_indent+ - nl_indent.join(defines) - +")") + if target_type == "static": + srcs = enumerate_srcs(name, target_name, srcs=target_info["srcs"]) + print("add_library(" + target_name + " STATIC EXCLUDE_FROM_ALL" + nl_indent + srcs + ")") + if target_type == "shared": + srcs = enumerate_srcs(name, target_name, srcs=target_info["srcs"]) + print("add_library(" + target_name + " SHARED EXCLUDE_FROM_ALL" + nl_indent + srcs + ")") + if target_type == "object": + srcs = enumerate_srcs(name, target_name, srcs=target_info["srcs"]) + print("add_library(" + target_name + " OBJECT" + nl_indent + srcs + ")") + elif target_type == "interface": + print("add_library(" + target_name + " INTERFACE)") + public_decl_type = "INTERFACE" + elif target_type == "imported": + print("add_library(" + target_name + " UNKNOWN IMPORTED)") + public_decl_type = "INTERFACE" + elif target_type == "subdirectory": + if "cache" in target_info: + for (var_name, value) in target_info["cache"].items(): + if isinstance(value, bool): + print("set(" + var_name + " " + ("ON" if value else "OFF") + " CACHE INTERNAL \"\" FORCE)") + print("add_subdirectory(${" + target_name + "_SOURCE_DIR} ${" + target_name + "_BINARY_DIR})") + elif target_type == "cmake": + print("include(${PROJECT_SOURCE_DIR}/" + target_info["file"] + ")") - if "links" in target_info: - print("target_link_libraries("+name+" "+nl_indent+ - nl_indent.join(map(lambda x: public_decl_type + ' ' + x, target_info["links"])) - +")") + if target_type in ["static", "interface", "object"]: + generate_target_definition( + name, target_name, + public_decl_type=public_decl_type, + target_info=target_info, + src_dir=src_dir) - if "aliases" in target_info: - for (tgt, src) in target_info["aliases"].items(): - print("add_library("+tgt+" ALIAS "+src+")") + if target_n != len(target_infos) - 1: + print() + + done_nl = False + def do_nl(): + global done_nl + if not done_nl: + print() + done_nl = True + + if "aliases" in dep_info: + do_nl() + for (tgt, src) in dep_info["aliases"].items(): + print("add_library(" + tgt + " ALIAS " + src + ")") + + if "extra_cmake" in dep_info: + do_nl() + extra_cmakes = dep_info["extra_cmake"] + if isinstance(extra_cmakes, str): + extra_cmakes = [extra_cmakes] + for extra_cmake in extra_cmakes: + extra_cmake = target_relative_path(extra_cmake, name) + print("include(" + extra_cmake + ")") - if "extra_cmake" in target_info: - print() - print("include(${PROJECT_SOURCE_DIR}/" + target_info["extra_cmake"]+")") print() - diff --git a/README.md b/README.md index e9b6f61..388ced8 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,7 @@ First you need to declare a dependency in your `dependencies.json` file: "download_name": "json-3.7.0.zip", "target": { "type": "interface", - "public_includes": [ - "." - ] + "includes": "." } } } @@ -90,7 +88,7 @@ Additionally, you can override just the zip file by setting `JSON_ZIP`. ## `dependencies.json` by example -### Boost +### [Boost](https://www.boost.org/) This example downloads the Boost source zip and configures it by building the `system` library. @@ -105,13 +103,11 @@ This example downloads the Boost source zip and configures it by building the `s "srcs": [ "libs/system/src/error_code.cpp" ], - "public_includes": [ - "." + "includes": ".", + "defines": [ + "BOOST_ALL_NO_LIB", + "BOOST_ASIO_NO_TYPEID" ], - "public_defines": { - "BOOST_ALL_NO_LIB": "1", - "BOOST_ASIO_NO_TYPEID": "1" - }, "links": [ "$<$:bcrypt.dll>" ] @@ -120,6 +116,55 @@ This example downloads the Boost source zip and configures it by building the `s } ``` +### [Catch2](https://github.com/catchorg/Catch2) + +This example will download and configure the Catch testing framework for compilation. Note that the `configure` directive is used to create a `.cpp` file for the runner and also note that the path is prefixed with `:`, this means that the generation will take place into a subdirectory in `${CMAKE_CURRENT_BINARY_DIR}`. + +Additionally, two `extra_cmake` paths are given to be included by CMake after the target is configured. + +```json +{ + "Catch2": { + "git": { + "repository": "https://github.com/catchorg/Catch2.git", + "tag": "c8db4e77c4c9aa20776db162e982f1af693ab8fe" + }, + "configure": { + ":catch2.cpp": { + "type": "generate", + "content": "#include \"catch.hpp\"" + } + }, + "target": [ + { + "type": "interface", + "includes": "single_include/catch2", + "defines": [ + "CATCH_CONFIG_NO_CPP17_UNCAUGHT_EXCEPTIONS", + "CATCH_CONFIG_ENABLE_BENCHMARKING" + ], + "public_defines": { + "CATCH_CONFIG_CONSOLE_WIDTH": 300 + } + }, + { + "type": "object", + "name": "Runner", + "srcs": ":catch2.cpp", + "private_defines": [ + "CATCH_CONFIG_MAIN" + ], + "links": "Catch2" + } + ], + "extra_cmake": [ + "contrib/Catch.cmake", + "contrib/ParseAndAddCatchTests.cmake" + ] + } +} +``` + ### [Naios/continuable](https://github.com/Naios/continuable) This will [Naios/continuable](https://github.com/Naios/continuable) and it's dependency [Naios/function2](https://github.com/Naios/function2) create both targets as a CMake interface library and link `Function2` into `Continuable`. As a consumer, you can simply link against `Continuabe` and CMake will add `Function2` transitively fot you. @@ -133,12 +178,8 @@ This will [Naios/continuable](https://github.com/Naios/continuable) and it's dep }, "target": { "type": "interface", - "public_includes": [ - "include" - ], - "links": [ - "Function2" - ] + "includes": "include", + "links": "Function2" } }, "Function2": { @@ -148,9 +189,7 @@ This will [Naios/continuable](https://github.com/Naios/continuable) and it's dep }, "target": { "type": "interface", - "public_includes": [ - "include" - ] + "includes": "include" } } } @@ -174,9 +213,9 @@ This will download [Rogiel/PacketBuffer](https://github.com/Rogiel/PacketBuffer) } ``` -### Poco +### [Poco](https://pocoproject.org/) -This example will download the Poco library and include `Poco.cmake` for further configuration. You can use `Poco_SOURCE_DIR` and `Poco_BINARY_DIR` if needed to configure your target. +This example will download the Poco library and include `/Poco.cmake` for further configuration. You can use `Poco_SOURCE_DIR` and `Poco_BINARY_DIR` if needed to configure your target. Note that the file is an absolute paht (starting with `/`). This means that CMakeDependency will include this dependency from `${PROJECT_SOURCE_DIR}`. This allows very flexible build configuration for dependencies. @@ -188,12 +227,44 @@ This allows very flexible build configuration for dependencies. ], "target": { "type": "cmake", - "file": "Poco.cmake" + "file": "/Poco.cmake" } } } ``` +### [STB Image](https://github.com/nothings/stb) + +This example will download and configure STB Image for compilation. Note that the `configure` directive is used to create a `.c` file for compilation. Also note that the path is prefixed with `:`, this means that the generation will take place into a subdirectory in `${CMAKE_CURRENT_BINARY_DIR}`. + +```json +{ + "STB": { + "git": { + "repository": "https://github.com/nothings/stb.git", + "tag": "052dce117ed989848a950308bd99eef55525dfb1" + }, + "configure": { + ":stb_image.c": { + "type": "generate", + "content": "#include " + } + }, + "target": [ + { + "type": "static", + "name": "Image", + "srcs": ":stb_image.c", + "private_defines": [ + "STB_IMAGE_IMPLEMENTATION" + ], + "includes": "." + } + ] + } +} +``` + ## Advanced features ### Global cache variables