From 82b638c18d442cb50b7f24d3e5fad14e63082bc8 Mon Sep 17 00:00:00 2001 From: John Freeman Date: Tue, 5 Nov 2024 11:41:45 -0600 Subject: [PATCH 1/5] Fix internal Conan component requirements --- cmake/cupcake_add_library.cmake | 4 +- cmake/cupcake_project.cmake | 1 + cmake/data/install_cpp_info.cmake | 1 + cmake/data/project_cpp_info/CMakeLists.txt | 114 +++++++++++++++------ 4 files changed, 84 insertions(+), 36 deletions(-) diff --git a/cmake/cupcake_add_library.cmake b/cmake/cupcake_add_library.cmake index a0c0763..8309287 100644 --- a/cmake/cupcake_add_library.cmake +++ b/cmake/cupcake_add_library.cmake @@ -36,9 +36,6 @@ function(cupcake_add_library name) if(name STREQUAL PROJECT_NAME) add_library(${PROJECT_NAME}.library ALIAS ${target}) endif() - set_target_properties(${target} PROPERTIES - EXPORT_NAME libraries::${name} - ) target_link_libraries(${PROJECT_NAME}.libraries INTERFACE ${target}) @@ -112,6 +109,7 @@ function(cupcake_add_library name) endif() if(NOT arg_PRIVATE) + set_target_properties(${target} PROPERTIES EXPORT_NAME libraries::${name}) set(alias ${PROJECT_NAME}::libraries::${name}) add_library(${alias} ALIAS ${target}) add_library(${PROJECT_NAME}::l::${name} ALIAS ${target}) diff --git a/cmake/cupcake_project.cmake b/cmake/cupcake_project.cmake index 502e768..8df2d2a 100644 --- a/cmake/cupcake_project.cmake +++ b/cmake/cupcake_project.cmake @@ -101,6 +101,7 @@ macro(cupcake_project) set(target ${PROJECT_NAME}.imports.main) add_library(${target} INTERFACE) + set_target_properties(${target} PROPERTIES EXPORT_NAME imports::main) install(TARGETS ${target} EXPORT ${PROJECT_EXPORT_SET}) add_library(${PROJECT_NAME}::imports::main ALIAS ${target}) diff --git a/cmake/data/install_cpp_info.cmake b/cmake/data/install_cpp_info.cmake index 1eaeaaf..a43fb05 100644 --- a/cmake/data/install_cpp_info.cmake +++ b/cmake/data/install_cpp_info.cmake @@ -48,5 +48,6 @@ execute_process( "-DCMAKE_INSTALL_PREFIX=${CMAKE_INSTALL_PREFIX}" "${CUPCAKE_MODULE_DIR}/data/project_cpp_info" WORKING_DIRECTORY "${tmp_dir}" + COMMAND_ERROR_IS_FATAL ANY ) file(REMOVE_RECURSE "${tmp_dir}") diff --git a/cmake/data/project_cpp_info/CMakeLists.txt b/cmake/data/project_cpp_info/CMakeLists.txt index ed9bb15..6cc1687 100644 --- a/cmake/data/project_cpp_info/CMakeLists.txt +++ b/cmake/data/project_cpp_info/CMakeLists.txt @@ -23,7 +23,13 @@ string(APPEND cpp_info "self.set_property('cmake_find_mode', 'config')\n" ) -set(target_pattern "^([^:]+)::(.+)$") +unset(buffer.log) +macro(log level text) + message(${level} "${text}") + string(APPEND buffer.log "# [${level}] ${text}\n") +endmacro() + +set(pattern_scope "^([^:]+)::(.+)$") # For each library target, add Python statements to the script text # to declare these properties on a component named after the target: @@ -36,9 +42,23 @@ set(target_pattern "^([^:]+)::(.+)$") # - sharedlinkflags: list of linker flags for shared libraries # - libs: singleton list of installed library name to be searched by linker # - libdirs: singleton list of directory, relative to the install prefix, where library is installed -foreach(target ${${PACKAGE_NAME}_LIBRARIES}) + +# `libraries` is the set of libraries that +# have been or will be added as Conan components. +set(libraries ${${PACKAGE_NAME}_LIBRARIES}) +# `targets` is the subset of `libraries` that +# have not yet been added as Conan components. +set(targets ${libraries}) +# On each iteration, we will remove one target from `targets`, +# and add zero or more targets to `targets` and `libraries` +# if they do not already appear in `libraries`. +while(targets) + list(POP_FRONT targets target) + + # log(STATUS "target = ${target}") + # Peel off the namespace to get the unqualified CMake target name. - string(REGEX MATCH "${target_pattern}" match ${target}) + string(REGEX MATCH "${pattern_scope}" match ${target}) if(NOT CMAKE_MATCH_1 STREQUAL PACKAGE_NAME) message(FATAL_ERROR "foreign target: '${target}'") endif() @@ -46,21 +66,22 @@ foreach(target ${${PACKAGE_NAME}_LIBRARIES}) # Target name unqualified by package name. set(tname ${CMAKE_MATCH_2}) - string(REGEX MATCH "${target_pattern}" match ${tname}) - if(NOT CMAKE_MATCH_1 STREQUAL "libraries") + string(REGEX MATCH "${pattern_scope}" match ${tname}) + set(type ${CMAKE_MATCH_1}) + if(NOT (type STREQUAL "libraries" OR type STREQUAL "imports")) message(FATAL_ERROR "not a library: '${target}'") endif() # Library name. set(name ${CMAKE_MATCH_2}) # Component name cannot contain double colon (::). - set(cname libraries.${name}) - set(components "${components}'${cname}',") + set(cname ${type}.${name}) + string(APPEND components "'${cname}',") - # "${cname}" == "libraries.${name}" - set(aliases "'${PACKAGE_NAME}::l::${name}'") - if(name STREQUAL PACKAGE_NAME) - set(aliases "${aliases},'${PACKAGE_NAME}::library'") + string(SUBSTRING ${type} 0 1 shorthand) + set(aliases "'${PACKAGE_NAME}::${shorthand}::${name}'") + if(type STREQUAL "libraries" AND name STREQUAL PACKAGE_NAME) + string(APPEND aliases ",'${PACKAGE_NAME}::library'") endif() string(APPEND cpp_info @@ -73,28 +94,54 @@ foreach(target ${${PACKAGE_NAME}_LIBRARIES}) ) get_target_property(links ${target} INTERFACE_LINK_LIBRARIES) - if(links) - unset(requires) - foreach(link ${links}) - if(NOT TARGET ${link}) - message(STATUS "ignoring non-target link '${link}' of target '${target}'") - continue() + unset(requires) + while(links) + list(POP_FRONT links link) + # Each link is one of: + # - An internal target. + # - An external target. + # - A non-target, most likely a library name or absolute path. + if(NOT TARGET ${link}) + log(ERROR "ignoring non-target link '${link}' of target '${target}'") + continue() + endif() + # Unalias once we've confirmed that it is a target. + get_target_property(linked ${link} ALIASED_TARGET) + if(linked) + set(link ${linked}) + endif() + # log(STATUS "link = ${link}") + # An internal target starts with "${PACKAGE_NAME}::". + # Everything else is an external target. + # We cannot distinguish internal vs external libraries by whether they are + # imported. It does not work when "importing" external libraries with + # `add_subdirectory`. The subdirectory's targets would not be imported, + # but would not be internal either, unless we install them. + string(REGEX MATCH "${pattern_scope}" match ${link}) + if(CMAKE_MATCH_1 STREQUAL PACKAGE_NAME) + # Internal target. + # Add it to `targets` (and `libraries`) if not yet in `libraries`. + list(FIND libraries "${link}" index) + if(index LESS 0) + list(APPEND libraries "${link}") + list(APPEND targets "${link}") endif() - # TODO: We could try to distinguish internal vs external libraries by - # whether they are imported, but that would not work when taking - # dependencies by add_subdirectory unless we install their libraries as - # internal libraries. - string(REGEX MATCH "${target_pattern}" match ${link}) - if(CMAKE_MATCH_1 STREQUAL PACKAGE_NAME) - set(lname ${CMAKE_MATCH_2}) - else() - get_target_property(lname ${link} ALIASED_TARGET) - if(NOT lname) - set(lname ${link}) - endif() + # Translate to Conan component name. + if(NOT CMAKE_MATCH_2 MATCHES "${pattern_scope}") + message(FATAL_ERROR "illegal internal target name: ${link}") endif() - string(APPEND requires "'${lname}',") - endforeach() + set(lname ${CMAKE_MATCH_1}.${CMAKE_MATCH_2}) + else() + # External target. + # TODO: Find Conan component name in variable map `CUPCAKE_CONAN_COMPONENT`. + # TODO: Pre-populate `CUPCAKE_CONAN_COMPONENT` from `cupcake.json`. + set(lname ${link}) + endif() + list(APPEND requires "'${lname}'") + endwhile() + if(requires) + list(REMOVE_DUPLICATES requires) + list(JOIN requires "," requires) string(APPEND cpp_info "self.components['${cname}'].requires = [${requires}]\n" ) @@ -163,11 +210,12 @@ foreach(target ${${PACKAGE_NAME}_LIBRARIES}) ) endif() endif() -endforeach() +endwhile() string(APPEND cpp_info - "self.components['libraries'].set_property('cmake_target_name', 'zero::libraries')\n" + "self.components['libraries'].set_property('cmake_target_name', '${PACKAGE_NAME}::libraries')\n" "self.components['libraries'].requires = [${components}]\n" + "${buffer.log}" ) # There is no way to model executables in `cpp_info`. From 8ce3a1acaa76bf8d297662834058bfadc2e9c721 Mon Sep 17 00:00:00 2001 From: John Freeman Date: Wed, 6 Nov 2024 10:27:53 -0600 Subject: [PATCH 2/5] Slim down with cupcake_json_get_list --- cmake/cupcake_add_executables.cmake | 65 ++++++++++++----------------- cmake/cupcake_add_libraries.cmake | 65 ++++++++++++----------------- cmake/cupcake_add_tests.cmake | 49 +++++++++------------- cmake/cupcake_find_packages.cmake | 38 +++++++---------- cmake/cupcake_json.cmake | 14 +++++-- cmake/cupcake_link_libraries.cmake | 36 +++++++--------- 6 files changed, 112 insertions(+), 155 deletions(-) diff --git a/cmake/cupcake_add_executables.cmake b/cmake/cupcake_add_executables.cmake index 2abcbde..cc34897 100644 --- a/cmake/cupcake_add_executables.cmake +++ b/cmake/cupcake_add_executables.cmake @@ -7,49 +7,36 @@ include(cupcake_json) function(cupcake_add_executables) cupcake_assert_special() - # executables = metadata.get('executables', []): - # executables :: [{ name :: string, links? :: array }] - cupcake_json_get(executables ARRAY "[]" "${PROJECT_JSON}" executables) - # for executable in executables: - string(JSON count LENGTH "${executables}") - if(count GREATER 0) - math(EXPR stop "${count} - 1") - foreach(i RANGE ${stop}) - # executable :: { name :: string, private? :: boolean, links :: array } - string(JSON executable GET "${executables}" ${i}) + # executables = metadata.get('executables', []) + cupcake_json_get_list(executables "${PROJECT_JSON}" executables) + foreach(executable IN LISTS executables) + # executable :: { name :: string, private? :: boolean, links :: array } - # If ${name} is a JSON string, it is unquoted here. - string(JSON name GET "${executable}" name) - cupcake_json_get(private BOOLEAN "false" "${executable}" private) - if(private) - set(private PRIVATE) - else() - set(private) - endif() - cupcake_add_executable(${name} ${private} "${ARGN}") + # If ${name} is a JSON string, it is unquoted here. + string(JSON name GET "${executable}" name) + cupcake_json_get(private BOOLEAN "false" "${executable}" private) + if(private) + set(private PRIVATE) + else() + set(private) + endif() + cupcake_add_executable(${name} ${private} "${ARGN}") - cupcake_json_get(links ARRAY "[]" "${executable}" links) - # links :: [ + cupcake_json_get_list(links "${executable}" links) + foreach(link IN LISTS links) + # link :: # | string # | { target :: string, scope? :: PUBLIC | PRIVATE | INTERFACE } - # ] - string(JSON count LENGTH "${links}") - if(count GREATER 0) - math(EXPR stop "${count} - 1") - foreach(j RANGE ${stop}) - string(JSON link GET "${links}" ${j}) - string(JSON type TYPE "${links}" ${j}) - if(type STREQUAL STRING) - set(target "${link}") - set(scope "PRIVATE") - else() - string(JSON target GET "${link}" target) - cupcake_json_get(scope STRING "PRIVATE" "${link}" scope) - endif() - cmake_language(EVAL CODE "set(target ${target})") - target_link_libraries(${this} ${scope} ${target}) - endforeach() + string(JSON type TYPE "${link}") + if(type STREQUAL STRING) + set(target "${link}") + set(scope "PRIVATE") + else() + string(JSON target GET "${link}" target) + cupcake_json_get(scope STRING "PRIVATE" "${link}" scope) endif() + cmake_language(EVAL CODE "set(target ${target})") + target_link_libraries(${this} ${scope} ${target}) endforeach() - endif() + endforeach() endfunction() diff --git a/cmake/cupcake_add_libraries.cmake b/cmake/cupcake_add_libraries.cmake index d2659f9..8a513b5 100644 --- a/cmake/cupcake_add_libraries.cmake +++ b/cmake/cupcake_add_libraries.cmake @@ -7,49 +7,36 @@ include(cupcake_json) function(cupcake_add_libraries) cupcake_assert_special() - # libraries = metadata.get('libraries', []): - # libraries :: [{ name :: string, links? :: array }] - cupcake_json_get(libraries ARRAY "[]" "${PROJECT_JSON}" libraries) - # for library in libraries: - string(JSON count LENGTH "${libraries}") - if(count GREATER 0) - math(EXPR stop "${count} - 1") - foreach(i RANGE ${stop}) - # library :: { name :: string, private? :: boolean, links :: array } - string(JSON library GET "${libraries}" ${i}) + # libraries = metadata.get('libraries', []) + cupcake_json_get_list(libraries "${PROJECT_JSON}" libraries) + foreach(library IN LISTS libraries) + # library :: { name :: string, private? :: boolean, links :: array } - # If ${name} is a JSON string, it is unquoted here. - string(JSON name GET "${library}" name) - cupcake_json_get(private BOOLEAN "false" "${library}" private) - if(private) - set(private PRIVATE) - else() - set(private) - endif() - cupcake_add_library(${name} ${private} "${ARGN}") + # If ${name} is a JSON string, it is unquoted here. + string(JSON name GET "${library}" name) + cupcake_json_get(private BOOLEAN "false" "${library}" private) + if(private) + set(private PRIVATE) + else() + set(private) + endif() + cupcake_add_library(${name} ${private} "${ARGN}") - cupcake_json_get(links ARRAY "[]" "${library}" links) - # links :: [ + cupcake_json_get_list(links "${library}" links) + foreach(link IN LISTS links) + # link :: # | string # | { target :: string, scope? :: PUBLIC | PRIVATE | INTERFACE } - # ] - string(JSON count LENGTH "${links}") - if(count GREATER 0) - math(EXPR stop "${count} - 1") - foreach(j RANGE ${stop}) - string(JSON link GET "${links}" ${j}) - string(JSON type TYPE "${links}" ${j}) - if(type STREQUAL STRING) - set(target "${link}") - set(scope "PUBLIC") - else() - string(JSON target GET "${link}" target) - cupcake_json_get(scope STRING "PUBLIC" "${link}" scope) - endif() - cmake_language(EVAL CODE "set(target ${target})") - target_link_libraries(${this} ${scope} ${target}) - endforeach() + string(JSON type TYPE "${link}") + if(type STREQUAL STRING) + set(target "${link}") + set(scope "PUBLIC") + else() + string(JSON target GET "${link}" target) + cupcake_json_get(scope STRING "PUBLIC" "${link}" scope) endif() + cmake_language(EVAL CODE "set(target ${target})") + target_link_libraries(${this} ${scope} ${target}) endforeach() - endif() + endforeach() endfunction() diff --git a/cmake/cupcake_add_tests.cmake b/cmake/cupcake_add_tests.cmake index 02710a7..1d3532b 100644 --- a/cmake/cupcake_add_tests.cmake +++ b/cmake/cupcake_add_tests.cmake @@ -6,40 +6,29 @@ include(cupcake_json) function(cupcake_add_tests) cupcake_assert_special() - cupcake_json_get(tests ARRAY "[]" "${PROJECT_JSON}" tests) - # tests :: [{ name :: string, links? :: array }] - string(JSON count LENGTH "${tests}") - if(count GREATER 0) - math(EXPR stop "${count} - 1") - foreach(i RANGE ${stop}) - string(JSON test GET "${tests}" ${i}) + cupcake_json_get_list(tests "${PROJECT_JSON}" tests) + foreach(test IN LISTS tests) + # test :: { name :: string, links? :: array } - string(JSON name GET "${test}" name) - # If ${name} is a JSON string, it is unquoted here. - cupcake_add_test(${name} "${ARGN}") + string(JSON name GET "${test}" name) + # If ${name} is a JSON string, it is unquoted here. + cupcake_add_test(${name} "${ARGN}") - cupcake_json_get(links ARRAY "[]" "${test}" links) - # links :: [ + cupcake_json_get_list(links "${test}" links) + foreach(link IN LISTS links) + # link :: # | string # | { target :: string, scope? :: PUBLIC | PRIVATE | INTERFACE } - # ] - string(JSON count LENGTH "${links}") - if(count GREATER 0) - math(EXPR stop "${count} - 1") - foreach(j RANGE ${stop}) - string(JSON link GET "${links}" ${j}) - string(JSON type TYPE "${links}" ${j}) - if(type STREQUAL STRING) - set(target "${link}") - set(scope "PRIVATE") - else() - string(JSON target GET "${link}" target) - cupcake_json_get(scope STRING "PRIVATE" "${link}" scope) - endif() - cmake_language(EVAL CODE "set(target ${target})") - target_link_libraries(${this} ${scope} ${target}) - endforeach() + string(JSON type TYPE "${link}") + if(type STREQUAL STRING) + set(target "${link}") + set(scope "PRIVATE") + else() + string(JSON target GET "${link}" target) + cupcake_json_get(scope STRING "PRIVATE" "${link}" scope) endif() + cmake_language(EVAL CODE "set(target ${target})") + target_link_libraries(${this} ${scope} ${target}) endforeach() - endif() + endforeach() endfunction() diff --git a/cmake/cupcake_find_packages.cmake b/cmake/cupcake_find_packages.cmake index 6286e69..45f89d3 100644 --- a/cmake/cupcake_find_packages.cmake +++ b/cmake/cupcake_find_packages.cmake @@ -8,26 +8,20 @@ include(cupcake_json) # TODO: Default `group` to `main`. function(cupcake_find_packages group) cupcake_assert_special() - cupcake_json_get(imports ARRAY "[]" "${PROJECT_JSON}" imports) - # imports :: [{ file :: string?, targets :: [string], groups :: [string] }] - string(JSON count LENGTH "${imports}") - if(count GREATER 0) - math(EXPR stop "${count} - 1") - foreach(i RANGE ${stop}) - string(JSON import GET "${imports}" ${i}) - # Select only imports whose `groups` (default: `["main"]`) - # contain `group`. - cupcake_json_get(groups ARRAY "[\"main\"]" "${import}" groups) - cupcake_json_to_list(groups "${groups}") - list(FIND groups "${group}" j) - if(j LESS 0) - continue() - endif() - string(JSON file GET "${import}" file) - if(NOT file) - string(JSON file GET "${import}" name) - endif() - cupcake_find_package("${file}" "${ARGN}") - endforeach() - endif() + cupcake_json_get_list(imports "${PROJECT_JSON}" imports) + foreach(import IN LISTS imports) + # import :: { file :: string?, targets :: [string], groups :: [string] } + # Select only imports whose `groups` (default: `["main"]`) contain `group`. + cupcake_json_get(groups ARRAY "[\"main\"]" "${import}" groups) + cupcake_json_to_list(groups "${groups}") + list(FIND groups "${group}" i) + if(i LESS 0) + continue() + endif() + string(JSON file GET "${import}" file) + if(NOT file) + string(JSON file GET "${import}" name) + endif() + cupcake_find_package("${file}" "${ARGN}") + endforeach() endfunction() diff --git a/cmake/cupcake_json.cmake b/cmake/cupcake_json.cmake index 77fb143..9b59603 100644 --- a/cmake/cupcake_json.cmake +++ b/cmake/cupcake_json.cmake @@ -19,15 +19,21 @@ function(cupcake_json_get variable type default json) set(${variable} "${value}" PARENT_SCOPE) endfunction() -function(cupcake_json_to_list list array) - set(tmp "") +function(cupcake_json_to_list variable array) + unset(list) string(JSON count LENGTH "${array}") if(count GREATER 0) math(EXPR stop "${count} - 1") foreach(i RANGE ${stop}) string(JSON item GET "${array}" ${i}) - list(APPEND tmp "${item}") + list(APPEND list "${item}") endforeach() endif() - set(${list} "${tmp}" PARENT_SCOPE) + set(${variable} "${list}" PARENT_SCOPE) +endfunction() + +function(cupcake_json_get_list variable json) + cupcake_json_get(list ARRAY "[]" "${json}" ${ARGN}) + cupcake_json_to_list(list "${list}") + set(${variable} "${list}" PARENT_SCOPE) endfunction() diff --git a/cmake/cupcake_link_libraries.cmake b/cmake/cupcake_link_libraries.cmake index d19f9af..c98fa92 100644 --- a/cmake/cupcake_link_libraries.cmake +++ b/cmake/cupcake_link_libraries.cmake @@ -8,25 +8,19 @@ include(cupcake_json) # TODO: Default `group` to `main`. function(cupcake_link_libraries target scope group) cupcake_assert_special() - cupcake_json_get(imports ARRAY "[]" "${PROJECT_JSON}" imports) - # imports :: [{ file :: string, targets :: [string] }] - string(JSON count LENGTH "${imports}") - if(count GREATER 0) - math(EXPR stop "${count} - 1") - foreach(i RANGE ${stop}) - string(JSON import GET "${imports}" ${i}) - # Select only imports whose `groups` (default: `["main"]`) - # contain `group`. - cupcake_json_get(groups ARRAY "[\"main\"]" "${import}" groups) - cupcake_json_to_list(groups "${groups}") - list(FIND groups "${group}" j) - if(j LESS 0) - continue() - endif() - string(JSON name GET "${import}" name) - cupcake_json_get(targets ARRAY "[\"${name}::${name}\"]" "${import}" targets) - cupcake_json_to_list(targets "${targets}") - target_link_libraries(${target} ${scope} ${targets}) - endforeach() - endif() + cupcake_json_get_list(imports "${PROJECT_JSON}" imports) + foreach(import IN LISTS imports) + # import :: { file :: string, targets :: [string] } + # Select only imports whose `groups` (default: `["main"]`) contain `group`. + cupcake_json_get(groups ARRAY "[\"main\"]" "${import}" groups) + cupcake_json_to_list(groups "${groups}") + list(FIND groups "${group}" i) + if(i LESS 0) + continue() + endif() + string(JSON name GET "${import}" name) + cupcake_json_get(targets ARRAY "[\"${name}::${name}\"]" "${import}" targets) + cupcake_json_to_list(targets "${targets}") + target_link_libraries(${target} ${scope} ${targets}) + endforeach() endfunction() From 1f77ca11cf364ad14facb798b8cec035f1d1c6db Mon Sep 17 00:00:00 2001 From: John Freeman Date: Wed, 6 Nov 2024 10:29:35 -0600 Subject: [PATCH 3/5] Fix external Conan component requirements When generating Conan components as part of installing `cpp_info.py`, fix the mapping from external CMake targets to external Conan components. --- cmake/cupcake_install_cpp_info.cmake | 4 ++ cmake/cupcake_project.cmake | 45 ++++++++++++++ cmake/data/install_cpp_info.cmake | 3 + cmake/data/project_cpp_info/CMakeLists.txt | 70 +++++++++++++--------- 4 files changed, 94 insertions(+), 28 deletions(-) diff --git a/cmake/cupcake_install_cpp_info.cmake b/cmake/cupcake_install_cpp_info.cmake index ea9112f..b4ae729 100644 --- a/cmake/cupcake_install_cpp_info.cmake +++ b/cmake/cupcake_install_cpp_info.cmake @@ -1,6 +1,7 @@ include_guard(GLOBAL) include(cupcake_module_dir) +include(cupcake_project_properties) file(READ "${CUPCAKE_MODULE_DIR}/data/install_cpp_info.cmake" CUPCAKE_INSTALL_CPP_INFO @@ -18,8 +19,11 @@ file(READ "${CUPCAKE_MODULE_DIR}/data/install_cpp_info.cmake" # package with `find_package` and then generates and installs a `cpp_info.py` # under `CMAKE_INSTALL_PREFIX`. function(cupcake_install_cpp_info) + cupcake_get_project_property(CONAN_COMPONENTS) + string(REPLACE "\"" "\\\"" CONAN_COMPONENTS "${CONAN_COMPONENTS}") install( CODE " +set(CONAN_COMPONENTS \"${CONAN_COMPONENTS}\") set(CMAKE_BINARY_DIR \"${CMAKE_BINARY_DIR}\") set(PACKAGE_NAME ${PROJECT_NAME}) string(TOUPPER $ CONFIG) diff --git a/cmake/cupcake_project.cmake b/cmake/cupcake_project.cmake index 8df2d2a..8bbc22a 100644 --- a/cmake/cupcake_project.cmake +++ b/cmake/cupcake_project.cmake @@ -1,5 +1,49 @@ include_guard(GLOBAL) +include(cupcake_json) +include(cupcake_project_properties) + +# This is a function just to create a private scope for variables. +function(_cupcake_parse_json json) + set(members "\"\":null") + # imports = json.get('imports', []) + cupcake_json_get_list(imports "${json}" imports) + foreach(import IN LISTS imports) + # import :: { root :: object, components :: object[] } + + # if component := import['root']: + string(JSON component ERROR_VARIABLE error GET "${import}" root) + if(NOT error) + cupcake_parse_component(ms "${component}") + string(APPEND members "${ms}") + endif() + + # components = import.get('components', []) + cupcake_json_get_list(components "${import}" components) + foreach(component IN LISTS components) + # component :: { name :: string, target :: string, aliases: string[] } + cupcake_parse_component(ms "${component}") + string(APPEND members "${ms}") + endforeach() + + endforeach() + cupcake_set_project_property(PROPERTY CONAN_COMPONENTS "${members}") +endfunction() + +function(cupcake_parse_component variable component) + string(JSON name GET "${component}" name) + string(JSON target GET "${component}" target) + # members[target] = name + set(members ",\"${target}\":\"${name}\"") + # aliases = component.get('aliases', []): + cupcake_json_get_list(aliases "${component}" aliases) + foreach(alias IN LISTS aliases) + # members[alias] = name + string(APPEND members ",\"${alias}\":\"${name}\"") + endforeach() + set(${variable} "${members}" PARENT_SCOPE) +endfunction() + macro(cupcake_project) # Allow `install(CODE)` to use generator expressions. cmake_policy(SET CMP0087 NEW) @@ -126,6 +170,7 @@ macro(cupcake_project) if(EXISTS "${path}") file(READ "${path}" PROJECT_JSON) set(${PROJECT_NAME}_JSON "${PROJECT_JSON}") + _cupcake_parse_json("${PROJECT_JSON}") endif() set(${PROJECT_NAME}_FOUND 1) diff --git a/cmake/data/install_cpp_info.cmake b/cmake/data/install_cpp_info.cmake index a43fb05..cf4b18c 100644 --- a/cmake/data/install_cpp_info.cmake +++ b/cmake/data/install_cpp_info.cmake @@ -8,6 +8,7 @@ # is `CMAKE_INSTALL_PREFIX`, because it can change at install time. set(parameters + CONAN_COMPONENTS CMAKE_BINARY_DIR PACKAGE_NAME CONFIG @@ -25,6 +26,7 @@ if(missing_parameters) message(FATAL_ERROR "missing parameters: ${missing_parameters}") endif() +message(STATUS "CONAN_COMPONENTS = '${CONAN_COMPONENTS}'") message(STATUS "CMAKE_BINARY_DIR = '${CMAKE_BINARY_DIR}'") message(STATUS "PACKAGE_NAME = '${PACKAGE_NAME}'") message(STATUS "CONFIG = '${CONFIG}'") @@ -37,6 +39,7 @@ set(tmp_dir "${CMAKE_BINARY_DIR}/cpp_info") file(MAKE_DIRECTORY "${tmp_dir}") execute_process( COMMAND "${CMAKE_COMMAND}" + "-DCONAN_COMPONENTS=${CONAN_COMPONENTS}" "-DPACKAGE_NAME=${PACKAGE_NAME}" "-DCONFIG=${CONFIG}" # CMake complains if `CMAKE_BUILD_TYPE` is not set for diff --git a/cmake/data/project_cpp_info/CMakeLists.txt b/cmake/data/project_cpp_info/CMakeLists.txt index 6cc1687..e74ada0 100644 --- a/cmake/data/project_cpp_info/CMakeLists.txt +++ b/cmake/data/project_cpp_info/CMakeLists.txt @@ -23,13 +23,16 @@ string(APPEND cpp_info "self.set_property('cmake_find_mode', 'config')\n" ) -unset(buffer.log) macro(log level text) - message(${level} "${text}") + set(space " ") + if(level STREQUAL STATUS) + set(space "") + endif() + message(${level} "${space}${text}") string(APPEND buffer.log "# [${level}] ${text}\n") endmacro() -set(pattern_scope "^([^:]+)::(.+)$") +set(pattern_namespace "^([^:]+)::(.+)$") # For each library target, add Python statements to the script text # to declare these properties on a component named after the target: @@ -43,22 +46,22 @@ set(pattern_scope "^([^:]+)::(.+)$") # - libs: singleton list of installed library name to be searched by linker # - libdirs: singleton list of directory, relative to the install prefix, where library is installed -# `libraries` is the set of libraries that +# `targets` is the set of CMake library targets that # have been or will be added as Conan components. -set(libraries ${${PACKAGE_NAME}_LIBRARIES}) -# `targets` is the subset of `libraries` that +set(targets ${${PACKAGE_NAME}_LIBRARIES}) +# `queue` is the subset of `targets` that # have not yet been added as Conan components. -set(targets ${libraries}) -# On each iteration, we will remove one target from `targets`, -# and add zero or more targets to `targets` and `libraries` -# if they do not already appear in `libraries`. -while(targets) - list(POP_FRONT targets target) +set(queue ${targets}) +# On each iteration, we will remove one target from `queue`, +# and add zero or more targets to `queue` and `targets` +# if they do not already appear in `targets`. +while(queue) + list(POP_FRONT queue target) # log(STATUS "target = ${target}") # Peel off the namespace to get the unqualified CMake target name. - string(REGEX MATCH "${pattern_scope}" match ${target}) + string(REGEX MATCH "${pattern_namespace}" match ${target}) if(NOT CMAKE_MATCH_1 STREQUAL PACKAGE_NAME) message(FATAL_ERROR "foreign target: '${target}'") endif() @@ -66,7 +69,8 @@ while(targets) # Target name unqualified by package name. set(tname ${CMAKE_MATCH_2}) - string(REGEX MATCH "${pattern_scope}" match ${tname}) + # Type must be "libraries" or "imports". + string(REGEX MATCH "${pattern_namespace}" match ${tname}) set(type ${CMAKE_MATCH_1}) if(NOT (type STREQUAL "libraries" OR type STREQUAL "imports")) message(FATAL_ERROR "not a library: '${target}'") @@ -74,10 +78,13 @@ while(targets) # Library name. set(name ${CMAKE_MATCH_2}) - # Component name cannot contain double colon (::). + # Component name. Cannot contain double colon (::). set(cname ${type}.${name}) - string(APPEND components "'${cname}',") + if(type STREQUAL "libraries") + string(APPEND library_components "'${cname}',") + endif() + # Generate external aliases. string(SUBSTRING ${type} 0 1 shorthand) set(aliases "'${PACKAGE_NAME}::${shorthand}::${name}'") if(type STREQUAL "libraries" AND name STREQUAL PACKAGE_NAME) @@ -117,25 +124,28 @@ while(targets) # imported. It does not work when "importing" external libraries with # `add_subdirectory`. The subdirectory's targets would not be imported, # but would not be internal either, unless we install them. - string(REGEX MATCH "${pattern_scope}" match ${link}) + string(REGEX MATCH "${pattern_namespace}" match ${link}) if(CMAKE_MATCH_1 STREQUAL PACKAGE_NAME) # Internal target. - # Add it to `targets` (and `libraries`) if not yet in `libraries`. - list(FIND libraries "${link}" index) + # Add it to `queue` (and `targets`) if not yet in `targets`. + list(FIND targets "${link}" index) if(index LESS 0) - list(APPEND libraries "${link}") list(APPEND targets "${link}") + list(APPEND queue "${link}") endif() # Translate to Conan component name. - if(NOT CMAKE_MATCH_2 MATCHES "${pattern_scope}") + if(NOT CMAKE_MATCH_2 MATCHES "${pattern_namespace}") message(FATAL_ERROR "illegal internal target name: ${link}") endif() set(lname ${CMAKE_MATCH_1}.${CMAKE_MATCH_2}) else() # External target. - # TODO: Find Conan component name in variable map `CUPCAKE_CONAN_COMPONENT`. - # TODO: Pre-populate `CUPCAKE_CONAN_COMPONENT` from `cupcake.json`. - set(lname ${link}) + # Translate to Conan component name. + string(JSON lname ERROR_VARIABLE error GET "{${CONAN_COMPONENTS}}" "${link}") + if(error) + log(ERROR "${error}") + set(lname ${link}) + endif() endif() list(APPEND requires "'${lname}'") endwhile() @@ -181,10 +191,12 @@ while(targets) ) endif() + get_target_property(options ${target} INTERFACE_LINK_OPTIONS) # LINK_FLAGS until CMake 3.13, then LINK_OPTIONS. - get_target_property(options ${target} LINK_OPTIONS) - if(NOT options) - get_target_property(options ${target} LINK_FLAGS) + get_target_property(flags ${target} LINK_FLAGS) + if(flags) + log(WARNING "LINK_FLAGS are deprecated. Found on ${target}") + list(APPEND options "${flags}") endif() if(options) foreach(option ${options}) @@ -212,9 +224,11 @@ while(targets) endif() endwhile() +# TODO: Let other libraries be the default export. string(APPEND cpp_info "self.components['libraries'].set_property('cmake_target_name', '${PACKAGE_NAME}::libraries')\n" - "self.components['libraries'].requires = [${components}]\n" + "self.components['libraries'].set_property('default_export', True)\n" + "self.components['libraries'].requires = [${library_components}]\n" "${buffer.log}" ) From 0992a5e9c47f3ee4741eb0a5150e4e5a983bc830 Mon Sep 17 00:00:00 2001 From: John Freeman Date: Wed, 6 Nov 2024 15:08:45 -0600 Subject: [PATCH 4/5] Fix quoting --- cmake/cupcake_add_executables.cmake | 3 ++- cmake/cupcake_add_libraries.cmake | 3 ++- cmake/cupcake_add_tests.cmake | 3 ++- cmake/cupcake_find_packages.cmake | 3 ++- cmake/cupcake_json.cmake | 17 +++++++++++++++++ cmake/cupcake_link_libraries.cmake | 4 +++- 6 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cmake/cupcake_add_executables.cmake b/cmake/cupcake_add_executables.cmake index cc34897..f362ae4 100644 --- a/cmake/cupcake_add_executables.cmake +++ b/cmake/cupcake_add_executables.cmake @@ -29,7 +29,8 @@ function(cupcake_add_executables) # | { target :: string, scope? :: PUBLIC | PRIVATE | INTERFACE } string(JSON type TYPE "${link}") if(type STREQUAL STRING) - set(target "${link}") + # A JSON string is already quoted. + set(target ${link}) set(scope "PRIVATE") else() string(JSON target GET "${link}" target) diff --git a/cmake/cupcake_add_libraries.cmake b/cmake/cupcake_add_libraries.cmake index 8a513b5..00880b4 100644 --- a/cmake/cupcake_add_libraries.cmake +++ b/cmake/cupcake_add_libraries.cmake @@ -29,7 +29,8 @@ function(cupcake_add_libraries) # | { target :: string, scope? :: PUBLIC | PRIVATE | INTERFACE } string(JSON type TYPE "${link}") if(type STREQUAL STRING) - set(target "${link}") + # A JSON string is already quoted. + set(target ${link}) set(scope "PUBLIC") else() string(JSON target GET "${link}" target) diff --git a/cmake/cupcake_add_tests.cmake b/cmake/cupcake_add_tests.cmake index 1d3532b..55a101e 100644 --- a/cmake/cupcake_add_tests.cmake +++ b/cmake/cupcake_add_tests.cmake @@ -21,7 +21,8 @@ function(cupcake_add_tests) # | { target :: string, scope? :: PUBLIC | PRIVATE | INTERFACE } string(JSON type TYPE "${link}") if(type STREQUAL STRING) - set(target "${link}") + # A JSON string is already quoted. + set(target ${link}) set(scope "PRIVATE") else() string(JSON target GET "${link}" target) diff --git a/cmake/cupcake_find_packages.cmake b/cmake/cupcake_find_packages.cmake index 45f89d3..fe8fcb9 100644 --- a/cmake/cupcake_find_packages.cmake +++ b/cmake/cupcake_find_packages.cmake @@ -14,7 +14,8 @@ function(cupcake_find_packages group) # Select only imports whose `groups` (default: `["main"]`) contain `group`. cupcake_json_get(groups ARRAY "[\"main\"]" "${import}" groups) cupcake_json_to_list(groups "${groups}") - list(FIND groups "${group}" i) + # Group names are quoted JSON strings in the list. + list(FIND groups "\"${group}\"" i) if(i LESS 0) continue() endif() diff --git a/cmake/cupcake_json.cmake b/cmake/cupcake_json.cmake index 9b59603..a15ff53 100644 --- a/cmake/cupcake_json.cmake +++ b/cmake/cupcake_json.cmake @@ -26,6 +26,11 @@ function(cupcake_json_to_list variable array) math(EXPR stop "${count} - 1") foreach(i RANGE ${stop}) string(JSON item GET "${array}" ${i}) + string(JSON type TYPE "${array}" ${i}) + if(type STREQUAL STRING) + # CMake automatically unquotes JSON strings. Re-quote them here. + set(item "\"${item}\"") + endif() list(APPEND list "${item}") endforeach() endif() @@ -37,3 +42,15 @@ function(cupcake_json_get_list variable json) cupcake_json_to_list(list "${list}") set(${variable} "${list}" PARENT_SCOPE) endfunction() + +# Unquote all strings in a list. +# unquote(variable [string...]) +function(cupcake_unquote variable) + foreach(string ${ARGN}) + if(string MATCHES "^\"(.*)\"$") + set(string "${CMAKE_MATCH_1}") + endif() + string(APPEND list "${string}") + endforeach() + set(${variable} "${list}" PARENT_SCOPE) +endfunction() diff --git a/cmake/cupcake_link_libraries.cmake b/cmake/cupcake_link_libraries.cmake index c98fa92..3873bcd 100644 --- a/cmake/cupcake_link_libraries.cmake +++ b/cmake/cupcake_link_libraries.cmake @@ -14,13 +14,15 @@ function(cupcake_link_libraries target scope group) # Select only imports whose `groups` (default: `["main"]`) contain `group`. cupcake_json_get(groups ARRAY "[\"main\"]" "${import}" groups) cupcake_json_to_list(groups "${groups}") - list(FIND groups "${group}" i) + # Group names are quoted JSON strings in the list. + list(FIND groups "\"${group}\"" i) if(i LESS 0) continue() endif() string(JSON name GET "${import}" name) cupcake_json_get(targets ARRAY "[\"${name}::${name}\"]" "${import}" targets) cupcake_json_to_list(targets "${targets}") + cupcake_unquote(targets "${targets}") target_link_libraries(${target} ${scope} ${targets}) endforeach() endfunction() From bac33bb6fe41ee44fa96d87f8b856269ccd0ba52 Mon Sep 17 00:00:00 2001 From: John Freeman Date: Wed, 6 Nov 2024 15:11:58 -0600 Subject: [PATCH 5/5] Rename to EXTERNAL_CONAN_COMPONENTS --- cmake/cupcake_install_cpp_info.cmake | 6 +++--- cmake/cupcake_project.cmake | 2 +- cmake/data/install_cpp_info.cmake | 6 +++--- cmake/data/project_cpp_info/CMakeLists.txt | 13 +++++++++++-- conanfile.py | 1 + 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/cmake/cupcake_install_cpp_info.cmake b/cmake/cupcake_install_cpp_info.cmake index b4ae729..a004f9a 100644 --- a/cmake/cupcake_install_cpp_info.cmake +++ b/cmake/cupcake_install_cpp_info.cmake @@ -19,11 +19,11 @@ file(READ "${CUPCAKE_MODULE_DIR}/data/install_cpp_info.cmake" # package with `find_package` and then generates and installs a `cpp_info.py` # under `CMAKE_INSTALL_PREFIX`. function(cupcake_install_cpp_info) - cupcake_get_project_property(CONAN_COMPONENTS) - string(REPLACE "\"" "\\\"" CONAN_COMPONENTS "${CONAN_COMPONENTS}") + cupcake_get_project_property(EXTERNAL_CONAN_COMPONENTS) + string(REPLACE "\"" "\\\"" EXTERNAL_CONAN_COMPONENTS "${EXTERNAL_CONAN_COMPONENTS}") install( CODE " -set(CONAN_COMPONENTS \"${CONAN_COMPONENTS}\") +set(EXTERNAL_CONAN_COMPONENTS \"${EXTERNAL_CONAN_COMPONENTS}\") set(CMAKE_BINARY_DIR \"${CMAKE_BINARY_DIR}\") set(PACKAGE_NAME ${PROJECT_NAME}) string(TOUPPER $ CONFIG) diff --git a/cmake/cupcake_project.cmake b/cmake/cupcake_project.cmake index 8bbc22a..e175a39 100644 --- a/cmake/cupcake_project.cmake +++ b/cmake/cupcake_project.cmake @@ -27,7 +27,7 @@ function(_cupcake_parse_json json) endforeach() endforeach() - cupcake_set_project_property(PROPERTY CONAN_COMPONENTS "${members}") + cupcake_set_project_property(PROPERTY EXTERNAL_CONAN_COMPONENTS "${members}") endfunction() function(cupcake_parse_component variable component) diff --git a/cmake/data/install_cpp_info.cmake b/cmake/data/install_cpp_info.cmake index cf4b18c..b39f122 100644 --- a/cmake/data/install_cpp_info.cmake +++ b/cmake/data/install_cpp_info.cmake @@ -8,7 +8,7 @@ # is `CMAKE_INSTALL_PREFIX`, because it can change at install time. set(parameters - CONAN_COMPONENTS + # EXTERNAL_CONAN_COMPONENTS can be an empty string. CMAKE_BINARY_DIR PACKAGE_NAME CONFIG @@ -26,7 +26,7 @@ if(missing_parameters) message(FATAL_ERROR "missing parameters: ${missing_parameters}") endif() -message(STATUS "CONAN_COMPONENTS = '${CONAN_COMPONENTS}'") +message(STATUS "EXTERNAL_CONAN_COMPONENTS = '${EXTERNAL_CONAN_COMPONENTS}'") message(STATUS "CMAKE_BINARY_DIR = '${CMAKE_BINARY_DIR}'") message(STATUS "PACKAGE_NAME = '${PACKAGE_NAME}'") message(STATUS "CONFIG = '${CONFIG}'") @@ -39,7 +39,7 @@ set(tmp_dir "${CMAKE_BINARY_DIR}/cpp_info") file(MAKE_DIRECTORY "${tmp_dir}") execute_process( COMMAND "${CMAKE_COMMAND}" - "-DCONAN_COMPONENTS=${CONAN_COMPONENTS}" + "-DEXTERNAL_CONAN_COMPONENTS=${EXTERNAL_CONAN_COMPONENTS}" "-DPACKAGE_NAME=${PACKAGE_NAME}" "-DCONFIG=${CONFIG}" # CMake complains if `CMAKE_BUILD_TYPE` is not set for diff --git a/cmake/data/project_cpp_info/CMakeLists.txt b/cmake/data/project_cpp_info/CMakeLists.txt index e74ada0..5ab23b4 100644 --- a/cmake/data/project_cpp_info/CMakeLists.txt +++ b/cmake/data/project_cpp_info/CMakeLists.txt @@ -10,6 +10,12 @@ find_package( PATHS ${CMAKE_INSTALL_PREFIX} ) +# Do not warn for unused variables. +if(EXTERNAL_CONAN_COMPONENTS) +endif() +if(CMAKE_POLICY_DEFAULT_CMP0091) +endif() + string(APPEND cpp_info # - names[generator]: namespace name "self.names['cmake_find_package'] = '${PACKAGE_NAME}'\n" @@ -141,9 +147,12 @@ while(queue) else() # External target. # Translate to Conan component name. - string(JSON lname ERROR_VARIABLE error GET "{${CONAN_COMPONENTS}}" "${link}") + string( + JSON lname ERROR_VARIABLE error + GET "{${EXTERNAL_CONAN_COMPONENTS}}" "${link}" + ) if(error) - log(ERROR "${error}") + log(ERROR "unrecognized target linked by ${target}: ${link}") set(lname ${link}) endif() endif() diff --git a/conanfile.py b/conanfile.py index 2f84be9..116a90e 100644 --- a/conanfile.py +++ b/conanfile.py @@ -10,6 +10,7 @@ class Cupcake(ConanFile): license = 'ISC' author = 'John Freeman ' url = 'https://github.com/thejohnfreeman/cupcake.cmake' + description = 'CMake boilerplate for an opinionated project structure.' # TODO: The CMake helper requires the build_type setting to run its build # or install methods.