diff --git a/.cmake-format.yaml b/.cmake-format.yaml index 0fc7d53..89d3a7e 100644 --- a/.cmake-format.yaml +++ b/.cmake-format.yaml @@ -51,10 +51,10 @@ lint: min_statement_spacing: 1 max_statement_spacing: 2 max_returns: 6 - max_branches: 30 # Default target: 12 + max_branches: 40 # Default target: 12 max_arguments: 6 # Default target: 5 max_localvars: 15 - max_statements: 110 # Default target: 50 + max_statements: 120 # Default target: 50 encode: emit_byteorder_mark: false input_encoding: utf-8 diff --git a/ListTargets.cmake b/ListTargets.cmake index 3919ada..cd360e9 100644 --- a/ListTargets.cmake +++ b/ListTargets.cmake @@ -70,22 +70,32 @@ function(swift_list_targets out_var) set(all_targets) foreach(target IN LISTS x_all_targets) + get_target_property(type ${target} TYPE) if(x_TYPES) - get_target_property(type ${target} TYPE) if(NOT ${type} IN_LIST x_TYPES) continue() endif() endif() if(x_SWIFT_TYPES) - get_target_property(type ${target} SWIFT_TYPE) - if(NOT ${type} IN_LIST x_SWIFT_TYPES) + if (type STREQUAL "INTERFACE_LIBRARY") + get_target_property(swift_type ${target} INTERFACE_SWIFT_TYPE) + else() + get_target_property(swift_type ${target} SWIFT_TYPE) + endif() + + if(NOT ${swift_type} IN_LIST x_SWIFT_TYPES) continue() endif() endif() if(x_ONLY_THIS_REPO) - get_target_property(target_dir ${target} SOURCE_DIR) + if (type STREQUAL "INTERFACE_LIBRARY") + get_target_property(target_dir ${target} INTERFACE_SOURCE_DIR) + else() + get_target_property(target_dir ${target} SOURCE_DIR) + endif() + # This replacement makes sure that we only filter out third_party subdirectories which actually exist in the root project source dir - ie, a git repo cloned in to a path # which just so happens to contain third_party should not break this function string(REPLACE ${CMAKE_SOURCE_DIR} "" target_dir ${target_dir}) diff --git a/Sonarcloud.cmake b/Sonarcloud.cmake new file mode 100644 index 0000000..5fba790 --- /dev/null +++ b/Sonarcloud.cmake @@ -0,0 +1,215 @@ +# +# Copyright (C) 2022 Swift Navigation Inc. +# Contact: Swift Navigation +# +# This source is subject to the license found in the file 'LICENSE' which must +# be distributed together with this source. All other rights reserved. +# +# THIS CODE AND INFORMATION IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, +# EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE. +# + +# +# OVERVIEW +# ======== +# +# Offers a way to generate the necessary Sonarcloud project file from the +# information available via cmake build system. User can call on +# "generate_sonarcloud_project_properties" to produce this project file which +# outlines to Sonarcloud what files/directories are deemed source files and +# which files are test files. +# +# This module will work in conjunction with "TestTargets" and "SwiftTargets", +# and "ListTargets" to identify all Swift production source/test code through. +# This means that only targets created via "swift_add_*" functions that are +# categorized as "production" code will be included in the analysis. No other +# none C/C++ source code will be included in the Sonarcloud project properties +# file (at the moment). +# +# USAGE +# ===== +# +# To generate the Sonarcloud project file, simply call the following method at +# the end of your projects CMakeLists.txt file: +# +# generate_sonarcloud_project_properties(${CMAKE_BINARY_DIR}/sonar-project.properties) +# +# This will generate a sonar project file in your root build directory (please +# don't ever write this to your source directory). Prior to uploading the +# results to Sonarcloud, you will need to manually transform all absolute path +# references to relative paths. This manual step can be done easily with the +# following Linux command: +# +# sed -i -E "s|^(\s+)(${PWD}/)|\1|g" 'sonar-project.properties' +# +# The reason why this extra step is required and was not taken care by cmake, +# is because targets have generator expressions to them which can only be +# evaluated at the end of the configuration stage. At that point there is no +# find/replace functionality. +# + +include(ListTargets) + +set(_sonarcloud_newline "\\\n ") + +function(_transform_sonarcloud_source_files output_variable target) + # + # Based off of https://cmake.org/cmake/help/latest/prop_tgt/SOURCES.html we + # need to correctly interpret the SOURCES properties to be able to transform + # them into what sonarcloud project properties is comfortable with (ie. + # relative path from project source directory). + # + get_target_property(target_binary_dir ${target} BUILD_DIR) + get_target_property(target_source_dir ${target} SOURCE_DIR) + + unset(source_files) + foreach (source_file IN LISTS ARGN) + get_source_file_property(is_build_generated_file ${target_binary_dir}/${source_file} GENERATED) + + if(IS_ABSOLUTE ${source_file}) + list(APPEND source_files ${source_file}) + continue() + endif() + + if (is_build_generated_file) + list(APPEND source_files ${target_binary_dir}/${source_file}) + continue() + endif() + + if (EXISTS ${target_source_dir}/${source_file}) + list(APPEND source_files ${target_source_dir}/${source_file}) + continue() + endif() + + if (source_file MATCHES "^\\$<.+>$") + list(APPEND source_files "$<$:$${_sonarcloud_newline}>>") + continue() + endif() + + message(WARNING "Sonarcloud is missing source file \"${source_file}\" for target ${target}") + endforeach() + + set(${output_variable} ${source_files} PARENT_SCOPE) +endfunction() + +function(_transform_sonarcloud_include_directories output_variable target) + unset(include_directories) + + foreach (include_directory IN LISTS ARGN) + if(IS_ABSOLUTE ${include_directory}) + list(APPEND include_directories ${include_directory}) + continue() + endif() + + if (include_directory MATCHES "^\\$$") + # ignoring installation interfaces + continue() + endif() + + if (include_directory MATCHES "^\\$<.+>$") + list(APPEND include_directories "$<$:$${_sonarcloud_newline}>>") + continue() + endif() + + message(WARNING "Sonarcloud is missing include directory \"${include_directory}\" for target ${target}") + endforeach() + + set(${output_variable} ${include_directories} PARENT_SCOPE) +endfunction() + +function(_extract_sonarcloud_project_files output_project_source_files output_project_include_directories) + unset(project_source_files) + unset(project_include_directories) + + foreach (target IN LISTS ARGN) + get_target_property(target_type ${target} TYPE) + + unset(target_source_files) + unset(target_include_directories) + + if (NOT target_type STREQUAL "INTERFACE_LIBRARY") + get_target_property(target_source_files ${target} SOURCES) + if (target_source_files) + _transform_sonarcloud_source_files(target_source_files ${target} ${target_source_files}) + else() + unset(target_source_files) + endif() + + get_target_property(target_include_directories ${target} INCLUDE_DIRECTORIES) + if (target_include_directories) + _transform_sonarcloud_include_directories(target_include_directories ${target} ${target_include_directories}) + else() + unset(target_include_directories) + endif() + endif() + + get_target_property(target_interface_include_directories ${target} INTERFACE_INCLUDE_DIRECTORIES) + if (target_interface_include_directories) + _transform_sonarcloud_include_directories(target_interface_include_directories ${target} ${target_interface_include_directories}) + else() + unset(target_interface_include_directories) + endif() + + list(APPEND project_source_files ${target_source_files}) + list(APPEND project_include_directories ${target_include_directories}) + list(APPEND project_include_directories ${target_interface_include_directories}) + endforeach() + + if (project_source_files) + list(SORT project_source_files) + list(REMOVE_DUPLICATES project_source_files) + endif() + + if (project_include_directories) + list(SORT project_include_directories) + list(REMOVE_DUPLICATES project_include_directories) + endif() + + set(${output_project_source_files} ${project_source_files} PARENT_SCOPE) + set(${output_project_include_directories} ${project_include_directories} PARENT_SCOPE) +endfunction() + +function(generate_sonarcloud_project_properties sonarcloud_project_properties_path) + if (NOT IS_ABSOLUTE ${sonarcloud_project_properties_path}) + message(FATAL_ERROR "Function \"generate_sonarcloud_project_properties\"" + "only accepts absolute paths to avoid ambiguity") + endif() + + if (NOT ${PROJECT_SOURCE_DIR} STREQUAL ${CMAKE_CURRENT_SOURCE_DIR}) + return() + endif() + + swift_list_targets(source_targets + ONLY_THIS_REPO + SWIFT_TYPES + executable + library + ) + + swift_list_targets(test_targets + ONLY_THIS_REPO + SWIFT_TYPES + test + test_library + ) + + _extract_sonarcloud_project_files(source_source_files source_include_directories ${source_targets}) + _extract_sonarcloud_project_files(test_source_files test_include_directories ${test_targets}) + + set(sonarcloud_project_properties_content "sonar.sourceEncoding=UTF-8\n") + + set(source_files ${source_source_files} ${source_include_directories}) + list(JOIN source_files ",${_sonarcloud_newline}" sonar_sources) + string(APPEND sonarcloud_project_properties_content "sonar.inclusions=${_sonarcloud_newline}${sonar_sources}\n") + + set(test_files ${test_source_files}) + list(JOIN test_files ",${_sonarcloud_newline}" sonar_tests) + string(APPEND sonarcloud_project_properties_content "sonar.tests.inclusions=${_sonarcloud_newline}${sonar_tests}\n") + + file(GENERATE + OUTPUT "${sonarcloud_project_properties_path}" + CONTENT "${sonarcloud_project_properties_content}" + ) + +endfunction() diff --git a/SwiftTargets.cmake b/SwiftTargets.cmake index f6c325d..4d4a1b9 100644 --- a/SwiftTargets.cmake +++ b/SwiftTargets.cmake @@ -115,6 +115,9 @@ # C_EXTENSIONS_ON: C extensions are disabled by default, adding this keyword # enables C extensions. # +# SKIP_COMPILE_OPTIONS: will forgo invoking the swift_set_compile_options on +# the target. +# # SINGLE VALUE KEYWORDS # # C_STANDARD: allow uses to override the default C standard used in a target, @@ -156,6 +159,31 @@ define_property(TARGET BRIEF_DOCS "Swift target type" FULL_DOCS "For use by other modules in this repository which need to know the classification of target. One of executable, library, tool, tool_library, test, test_library") +define_property(TARGET + PROPERTY INTERFACE_SWIFT_TYPE + BRIEF_DOCS "Swift target type" + FULL_DOCS "Identical use as SWIFT_TYPE except that this applies to ALL target types, including INTERFACE") + +define_property(TARGET + PROPERTY SWIFT_PROJECT + BRIEF_DOCS "Swift project name" + FULL_DOCS "For use by other modules in this repository which need to know the project which this target belongs to") + +define_property(TARGET + PROPERTY INTERFACE_SWIFT_PROJECT + BRIEF_DOCS "Swift project name" + FULL_DOCS "Identical use as SWIFT_PROJECT except that this applies to ALL target types, including INTERFACE") + +define_property(TARGET + PROPERTY SWIFT_TEST_TYPE + BRIEF_DOCS "Swift test type" + FULL_DOCS "When target's SWIFT_PROJECT property is \"test\", this option, if set, will identify what type of test it is. Currently support \"unit\" or \"integration\"") + +define_property(TARGET + PROPERTY INTERFACE_SOURCE_DIR + BRIEF_DOCS "Target's source directory" + FULL_DOCS "Identical use as SOURCE_DIR except that this applies to ALL target types, including INTERFACE") + macro(swift_collate_arguments prefix name) set(exclusion_list ${ARGN}) set(${name}_args "") @@ -182,7 +210,7 @@ macro(swift_collate_arguments prefix name) endmacro() function(swift_add_target target type) - set(this_option INTERFACE OBJECT STATIC SHARED MODULE) + set(this_option INTERFACE OBJECT STATIC SHARED MODULE SKIP_COMPILE_OPTIONS) set(this_single "") set(this_multi SOURCES) @@ -288,11 +316,38 @@ function(swift_add_target target type) message(FATAL_ERROR "Unknown Swift target type ${type}") endif() + # + # This edge case is needed for cmake version < 3.19.0 where INTERFACE + # classes cannot contain any property other than those prefixed with + # "INTERFACE_". + # + # see: https://stackoverflow.com/questions/68502038/custom-properties-for-interface-libraries + # + # Until we migrate the cmake scripts to require 3.19.0, we should use the + # "INTERFACE_*" properties. If you want to go the extra mile, make sure to + # check both `INTERFACE_` and non `INTERFACE_` properties, later on we can + # delete the `INTERFACE_` once this illogical constraint is removed. + # + set_target_properties(${target} + PROPERTIES + INTERFACE_SWIFT_PROJECT ${PROJECT_NAME} + INTERFACE_SWIFT_TYPE ${type} + INTERFACE_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR} + ) + if (NOT x_INTERFACE) - set_target_properties(${target} PROPERTIES SWIFT_TYPE ${type}) - swift_set_compile_options(${target} ${compile_options_args} EXTRA_FLAGS ${extra_flags}) + set_target_properties(${target} + PROPERTIES + SWIFT_PROJECT ${PROJECT_NAME} + SWIFT_TYPE ${type} + ) + swift_set_language_standards(${target} ${language_standards_args}) target_code_coverage(${target} NO_RUN) + + if (NOT x_SKIP_COMPILE_OPTIONS) + swift_set_compile_options(${target} ${compile_options_args} EXTRA_FLAGS ${extra_flags}) + endif() endif() endfunction() diff --git a/TestTargets.cmake b/TestTargets.cmake index ba5a889..f5cf83d 100644 --- a/TestTargets.cmake +++ b/TestTargets.cmake @@ -332,7 +332,18 @@ function(swift_add_test target) add_dependencies(do-all-tests do-${target}) endif() + set_target_properties(${target} + PROPERTIES + SWIFT_PROJECT ${PROJECT_NAME} + INTERFACE_SWIFT_PROJECT ${PROJECT_NAME} + ) + if (x_INTEGRATION_TEST) + set_target_properties(${target} + PROPERTIES + SWIFT_TEST_TYPE integration + ) + if (NOT TARGET do-all-integration-tests) add_custom_target(do-all-integration-tests) endif() @@ -345,6 +356,11 @@ function(swift_add_test target) endif() if (x_UNIT_TEST) + set_target_properties(${target} + PROPERTIES + SWIFT_TEST_TYPE unit + ) + if (NOT TARGET do-all-unit-tests) add_custom_target(do-all-unit-tests) endif()