fprime/cmake/utilities.cmake
M Starch 74dd80e2b7
Add FPRIME_CMAKE_QUIET option (#4276)
* Add FPRIME_CMAKE_QUIET option

* Remove makefile deadcode

* Attempt CI debug

Add debugging message for FPRIME_SUB_BUILD_JOBS variable.

* Fix argument passing for sub-build jobs

* Update cmake/sub-build/sub-build.cmake

* Update cmake/sub-build/sub-build.cmake

---------

Co-authored-by: Thomas Boyer-Chammard <49786685+thomas-bc@users.noreply.github.com>
2025-10-08 16:30:36 -07:00

968 lines
40 KiB
CMake
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

####
# utilities.cmake:
#
# Utility and support functions for the fprime CMake build system.
####
include_guard()
set_property(GLOBAL PROPERTY C_CPP_ASM_REGEX ".*\.(c|cpp|cc|cxx|S|asm)$")
####
# Function `sort_buildable_from_non_buildable_sources`:
#
# Sorts C/C++ "buildable" sources from other sources. This uses the GLOBAL property C_CPP_ASM_REGEX to
# determine how to sort. Ideally users would use SOURCES and AUTOCODER_INPUTS to distinguish but this
# provides some backwards compatibility with the merged SOURCE_FILES variable.
#
# - **BUILDABLE_SOURCE_OUTPUT**: output name for buildable sources to be set in parent scope
# - **NON_BUILDABLE_SOURCE_OUTPUT**: output name for non-buildable sources to be set in parent scope
####
function(sort_buildable_from_non_buildable_sources BUILDABLE_SOURCE_OUTPUT NON_BUILDABLE_SOURCE_OUTPUT)
get_property(SORT_REGEX GLOBAL PROPERTY C_CPP_ASM_REGEX )
set(CPP_LIST_NAME ${ARGN})
set(NON_CPP_LIST_NAME ${ARGN})
list(FILTER CPP_LIST_NAME INCLUDE REGEX "${SORT_REGEX}")
list(FILTER NON_CPP_LIST_NAME EXCLUDE REGEX "${SORT_REGEX}")
set("${BUILDABLE_SOURCE_OUTPUT}" ${CPP_LIST_NAME} PARENT_SCOPE)
set("${NON_BUILDABLE_SOURCE_OUTPUT}" ${NON_CPP_LIST_NAME} PARENT_SCOPE)
endfunction()
####
# Macro `clear_historical_variables`:
#
# Clears old variables `MOD_DEPS`, `SOURCE_FILES`, `HEADER_FILES`, etc. from the scope of the
# caller. This removes accidental uses of these variables within the refactored system from this
# scope and below.
#
# This is a macro to ensure the caller's scope is affected.
#
# **ARGN:** passed to the `unset` calls (for things like PARENT_SCOPE)
####
function(clear_historical_variables)
unset(SOURCE_FILES ${ARGN})
unset(MOD_DEPS ${ARGN})
unset(HEADER_FILES ${ARGN})
unset(UT_SOURCE_FILES ${ARGN})
unset(UT_MOD_DEPS ${ARGN})
unset(UT_HEADER_FILES ${ARGN})
endfunction()
####
# Function `plugin_name`:
#
# From a plugin include path retrieve the plugin name. This is the name without any .cmake extension.
#
# INCLUDE_PATH: path to plugin
# OUTPUT_VARIABLE: variable to set in caller's scope with result
####
function(plugin_name INCLUDE_PATH OUTPUT_VARIABLE)
get_filename_component(TEMP_NAME "${INCLUDE_PATH}" NAME_WE)
set("${OUTPUT_VARIABLE}" ${TEMP_NAME} PARENT_SCOPE)
endfunction(plugin_name)
####
# Function `generate_individual_function_call`:
#
# Generates a routing table entry for the faux cmake_language call for an individual function. This call consists of
# a single `elseif(name == function and ARGC == ARG_COUNT)` to support a call to the function with ARG_COUNT arguments.
# This is a helper function intended for use within `generate_faux_cmake_language`.
#
# OUTPUT_FILE: file to write these `elseif` blocks into
# FUNCTION: name of function to write out
# ARG_COUNT: number of args for this particular invocation of the call
####
function(generate_individual_function_call OUTPUT_FILE FUNCTION ARG_COUNT)
# Build an invocation string of the form: ${FUNCTION}("${ARGV2}" "${ARGV3}" ..."${ARG_COUNT -1 + 2}")
# Notice several properties:
# 1. Calling function by a substituted name
# 2. Arguments are specifically escaped. Thus **must** be done to ensure that empty, and list arguments are
# correctly handled. Otherwise they randomly expand or disappear
# 3. Arg numbers start at 2. This accounts for ARGV0==CALL and ARGV1==Function name in the calling function
set(ARG_STRING "")
math(EXPR BOUND "${ARG_COUNT} - 1")
foreach(ARG_IT RANGE "${BOUND}")
math(EXPR ARG_NUM "${ARG_IT} + 2")
set(ARG_STRING "${ARG_STRING} \"\${ARGV${ARG_NUM}}\"")
endforeach()
math(EXPR ARG_NUM "${ARG_COUNT} + 2")
set(INVOCATION "${FUNCTION}(${ARG_STRING})")
file(APPEND "${FAUX_FILE}" " elseif (\"\${FUNCTION_NAME}\" STREQUAL ${FUNCTION} AND \${ARGC} EQUAL ${ARG_NUM})\n")
file(APPEND "${FAUX_FILE}" " ${INVOCATION}\n")
endfunction(generate_individual_function_call)
####
# Function `generate_faux_cmake_language`:
#
# This function is used to setup a fake implementation of `cmake_language` calls on implementations of CMake that
# predate its creation. The facsimile is incomplete, but for the purposes of this build system, it will be sufficient
# meaning that it can route all the plugin functions correctly but specifically **not** arbitrary function calls.
#
# Functions supported by this call are expected in the GLOBAL property: CMAKE_LANGUAGE_ROUTE_LIST
#
# This is accomplished by writing out a CMake file that contains a macro that looks like the `cmake_language(CALL)`
# feature but is implemented by an `if (NAME == FUNCTION) FUNCTION() endif()` table. This file is built within and
# included when finished.
#
# In terms of performance:
# - Native `cmake_language(CALL)` is incredibly fast
# - This faux implementation is slow
# - Repetitive including of .cmake files to "switch" implementations (as done in fprime v3.0.0) is **much** slower
####
function(generate_faux_cmake_language)
set(FAUX_FILE "${CMAKE_BINARY_DIR}/cmake_language.cmake")
set(ARG_MAX 5)
file(WRITE "${FAUX_FILE}" "#### AUTOGENERATED, DO NOT EDIT ####\n")
file(APPEND "${FAUX_FILE}" "macro(cmake_language ACTION FUNCTION_NAME)\n")
file(APPEND "${FAUX_FILE}" " if (NOT \"\${ACTION}\" STREQUAL \"CALL\")\n")
file(APPEND "${FAUX_FILE}" " message(FATAL_ERROR \"Cannot use \${ACTION} with faux cmake_language\")\n")
file(APPEND "${FAUX_FILE}" " elseif (NOT COMMAND \"\${FUNCTION_NAME}\")\n")
file(APPEND "${FAUX_FILE}" " message(FATAL_ERROR \"Unknown function \${FUNCTION_NAME} supplied to faux cmake_language\")\n")
# Generate one if block set for each function in the routing database
get_property(FUNCTIONS GLOBAL PROPERTY CMAKE_LANGUAGE_ROUTE_LIST)
foreach(FUNCTION IN LISTS FUNCTIONS)
math(EXPR ARG_TOP "${ARG_MAX} + 2")
fprime_cmake_debug_message("Mimicking cmake_language(CALL ${FUNCTION} \"\${ARGV2}\" ... \"\${ARGV${ARG_TOP}}\"")
foreach(ARG_COUNT RANGE "${ARG_MAX}")
generate_individual_function_call("${FAUX_FILE}" "${FUNCTION}" ${ARG_COUNT})
endforeach()
file(APPEND "${FAUX_FILE}" " elseif (\"\${FUNCTION_NAME}\" STREQUAL ${FUNCTION})\n")
file(APPEND "${FAUX_FILE}" " message(FATAL_ERROR \"Faux cmake_language called with too-many arguments: \${ARGC}\")\n")
endforeach()
file(APPEND "${FAUX_FILE}" " endif()\n")
file(APPEND "${FAUX_FILE}" "endmacro(cmake_language)\n")
include("${FAUX_FILE}")
endfunction()
####
# Function `plugin_include_helper`:
#
# Designed to help include API files (targets, autocoders) in an efficient way within CMake. This function imports a
# CMake file and defines a `dispatch_<function>(PLUGIN_NAME ...)` function for each function name in ARGN. Thus users
# of the imported plugin can call `dispatch_<function>(PLUGIN_NAME ...)` to dispatch a function as implemented in a
# plugin.
#
# OUTPUT_VARIABLE: set with the plugin name that has last been included
# INCLUDE_PATH: path to file to include
####
function(plugin_include_helper OUTPUT_VARIABLE INCLUDE_PATH)
plugin_name("${INCLUDE_PATH}" PLUGIN_NAME)
# Get the global property of all function items
get_property(TEMP_LIST GLOBAL PROPERTY CMAKE_LANGUAGE_ROUTE_LIST)
set(CHANGED FALSE)
foreach(PLUGIN_FUNCTION IN LISTS ARGN)
# Include the file if we have not found the prefixed function name yet
if (NOT COMMAND "${PLUGIN_NAME}_${PLUGIN_FUNCTION}")
include("${INCLUDE_PATH}")
endif()
# Add the function if any of the set were determined missing
if (NOT "${PLUGIN_NAME}_${PLUGIN_FUNCTION}" IN_LIST TEMP_LIST)
set_property(GLOBAL APPEND PROPERTY CMAKE_LANGUAGE_ROUTE_LIST "${PLUGIN_NAME}_${PLUGIN_FUNCTION}")
set(CHANGED TRUE)
endif()
endforeach()
# If cmake_language is not available, we have to implement it
if(CHANGED AND ${CMAKE_VERSION} VERSION_LESS "3.18.0")
generate_faux_cmake_language()
endif()
set("${OUTPUT_VARIABLE}" "${PLUGIN_NAME}" PARENT_SCOPE)
endfunction(plugin_include_helper)
####
# starts_with:
#
# Check if the string input starts with the given prefix. Sets OUTPUT_VAR to TRUE when it does and sets OUTPUT_VAR to
# FALSE when it does not. OUTPUT_VAR is the name of the variable in PARENT_SCOPE that will be set.
#
# Note: regexs in CMake are known to be inefficient. Thus `starts_with` and `ends_with` are implemented without them
# in order to ensure speed.
#
# OUTPUT_VAR: variable to set
# STRING: string to check
# PREFIX: expected ending
####
function(starts_with OUTPUT_VAR STRING PREFIX)
set("${OUTPUT_VAR}" FALSE PARENT_SCOPE)
string(LENGTH "${PREFIX}" PREFIX_LENGTH)
string(SUBSTRING "${STRING}" "0" "${PREFIX_LENGTH}" FOUND_PREFIX)
# Check the substring
if (FOUND_PREFIX STREQUAL "${PREFIX}")
set("${OUTPUT_VAR}" TRUE PARENT_SCOPE)
endif()
endfunction(starts_with)
####
# ends_with:
#
# Check if the string input ends with the given suffix. Sets OUTPUT_VAR to TRUE when it does and sets OUTPUT_VAR to
# FALSE when it does not. OUTPUT_VAR is the name of the variable in PARENT_SCOPE that will be set.
#
# Note: regexs in CMake are known to be inefficient. Thus `starts_with` and `ends_with` are implemented without them
# in order to ensure speed.
#
# OUTPUT_VAR: variable to set
# STRING: string to check
# SUFFIX: expected ending
####
function(ends_with OUTPUT_VAR STRING SUFFIX)
set("${OUTPUT_VAR}" FALSE PARENT_SCOPE)
string(LENGTH "${STRING}" INPUT_LENGTH)
string(LENGTH "${SUFFIX}" SUFFIX_LENGTH)
if (INPUT_LENGTH GREATER_EQUAL SUFFIX_LENGTH)
# Calculate the substring of suffix length at end of string
math(EXPR START "${INPUT_LENGTH} - ${SUFFIX_LENGTH}")
string(SUBSTRING "${STRING}" "${START}" "${SUFFIX_LENGTH}" FOUND_SUFFIX)
# Check the substring
if (FOUND_SUFFIX STREQUAL "${SUFFIX}")
set("${OUTPUT_VAR}" TRUE PARENT_SCOPE)
endif()
endif()
endfunction(ends_with)
####
# init_variables:
#
# Initialize all variables passed in to empty variables in the calling scope.
####
function(init_variables)
foreach (VARIABLE IN LISTS ARGN)
set(${VARIABLE} "" PARENT_SCOPE)
endforeach()
endfunction(init_variables)
####
# normalize_paths:
#
# Take in any number of lists of paths and normalize the paths returning a single list.
# OUTPUT_NAME: name of variable to set in parent scope
####
function(normalize_paths OUTPUT_NAME)
set(OUTPUT_LIST)
# Loop over the list and check
foreach (PATH_LIST IN LISTS ARGN)
foreach(PATH IN LISTS PATH_LIST)
get_filename_component(PATH "${PATH}" ABSOLUTE)
list(APPEND OUTPUT_LIST "${PATH}")
endforeach()
endforeach()
set(${OUTPUT_NAME} "${OUTPUT_LIST}" PARENT_SCOPE)
endfunction(normalize_paths)
####
# resolve_dependencies:
#
# Sets OUTPUT_VAR in parent scope to be the set of dependencies in canonical form: relative path from root replacing
# directory separators with "_". E.g. fprime/Fw/Time becomes Fw_Time.
#
# OUTPUT_VAR: variable to fill in parent scope
# ARGN: list of dependencies to resolve
####
function(resolve_dependencies OUTPUT_VAR)
# Resolve all dependencies
set(RESOLVED)
foreach(DEPENDENCY IN LISTS ARGN)
# No resolution is done on linker-only dependencies
linker_only(LINKER_ONLY "${DEPENDENCY}")
if (LINKER_ONLY)
list(APPEND RESOLVED "${DEPENDENCY}")
continue()
endif()
get_module_name(${DEPENDENCY})
if (NOT MODULE_NAME IN_LIST RESOLVED)
list(APPEND RESOLVED "${MODULE_NAME}")
endif()
endforeach()
set(${OUTPUT_VAR} "${RESOLVED}" PARENT_SCOPE)
endfunction(resolve_dependencies)
####
# Function `is_target_real`:
#
# Does this target represent a real item (executable, library)? OUTPUT is set to TRUE when real, and FALSE otherwise.
# Non-real targets include TARGET_TYPE=UTILITY and ALIASED_TARGET.
#
# OUTPUT: variable to set
# TEST_TARGET: target to set
####
function(is_target_real OUTPUT TEST_TARGET)
if (TARGET "${DEPENDENCY}")
get_target_property(TARGET_TYPE "${DEPENDENCY}" TYPE)
# Make sure this is not a utility target
get_target_property(IS_ALIAS "${TEST_TARGET}" ALIASED_TARGET)
if (NOT TARGET_TYPE STREQUAL "UTILITY" AND NOT IS_ALIAS)
set("${OUTPUT}" TRUE PARENT_SCOPE)
return()
endif()
endif()
set("${OUTPUT}" FALSE PARENT_SCOPE)
endfunction()
####
# Function `is_target_library`:
#
# Does this target represent a real library? OUTPUT is set to TRUE when real, and FALSE otherwise.
#
# OUTPUT: variable to set
# TEST_TARGET: target to set
####
function(is_target_library OUTPUT TEST_TARGET)
set("${OUTPUT}" FALSE PARENT_SCOPE)
if (TARGET "${TEST_TARGET}")
get_target_property(TARGET_TYPE "${TEST_TARGET}" TYPE)
ends_with(IS_LIBRARY "${TARGET_TYPE}" "_LIBRARY")
set("${OUTPUT}" "${IS_LIBRARY}" PARENT_SCOPE)
endif()
endfunction()
####
# linker_only:
#
# Checks if a given dependency should be supplied to the linker only. These will not be supplied as CMake dependencies
# but will be supplied as link libraries. These tokens are of several types:
#
# 1. Linker flags: starts with -l
# 2. Existing Files: accounts for preexisting libraries shared and otherwise
#
# OUTPUT_VAR: variable to set in PARENT_SCOPE to TRUE/FALSE
# TOKEN: token to check if "linker only"
####
function(linker_only OUTPUT_VAR TOKEN)
set("${OUTPUT_VAR}" FALSE PARENT_SCOPE)
starts_with(IS_LINKER_FLAG "${TOKEN}" "-l")
if (IS_LINKER_FLAG OR (EXISTS "${TOKEN}" AND NOT IS_DIRECTORY "${TOKEN}"))
set("${OUTPUT_VAR}" TRUE PARENT_SCOPE)
endif()
endfunction()
####
# build_relative_path:
#
# Calculate the path to an item relative to known build paths. Search is performed in the following order erring if the
# item is found in multiple paths.
#
# INPUT_PATH: input path to search
# OUTPUT_VAR: output variable to fill
####
function(build_relative_path INPUT_PATH OUTPUT_VAR)
# Implementation assertion
if (NOT DEFINED FPRIME_BUILD_LOCATIONS)
message(FATAL_ERROR "FPRIME_BUILD_LOCATIONS not set before build_relative_path was called")
endif()
normalize_paths(FPRIME_LOCS_NORM ${FPRIME_BUILD_LOCATIONS})
normalize_paths(INPUT_PATH ${INPUT_PATH})
foreach(PARENT IN LISTS FPRIME_LOCS_NORM)
string(REGEX REPLACE "${PARENT}/(.*)$" "\\1" LOC_TEMP "${INPUT_PATH}")
if (NOT LOC_TEMP STREQUAL INPUT_PATH AND NOT LOC_TEMP MATCHES "${LOC}$")
message(FATAL_ERROR "Found ${INPUT_PATH} at multiple locations: ${LOC} and ${LOC_TEMP}")
elseif(NOT LOC_TEMP STREQUAL INPUT_PATH AND NOT DEFINED LOC)
set(LOC "${LOC_TEMP}")
endif()
endforeach()
if (LOC STREQUAL "")
message(FATAL_ERROR "Failed to find location for: ${INPUT_PATH}")
endif()
set(${OUTPUT_VAR} ${LOC} PARENT_SCOPE)
endfunction(build_relative_path)
####
# on_any_changed:
#
# Sets VARIABLE to true if any file has been noted as changed from the "on_changed" function. Will create cache files
# in the binary directory. Please see: on_changed
#
# INPUT_FILES: files to check for changes
# ARGN: passed into execute_process via on_changed call
####
function (on_any_changed INPUT_FILES VARIABLE)
foreach(INPUT_FILE IN LISTS INPUT_FILES)
on_changed("${INPUT_FILE}" TEMP_ON_CHANGED ${ARGN})
if (TEMP_ON_CHANGED)
set(${VARIABLE} TRUE PARENT_SCOPE)
return()
endif()
endforeach()
set(${VARIABLE} FALSE PARENT_SCOPE)
endfunction()
####
# on_changed:
#
# Sets VARIABLE to true if and only if the given file has changed since the last time this function was invoked. It will
# create "${INPUT_FILE}.prev" in the binary directory as a cache from the previous invocation. The result is always TRUE
# unless a successful no-difference is calculated.
#
# INPUT_FILE: file to check if it has changed
# ARGN: passed into execute_process
####
function (on_changed INPUT_FILE VARIABLE)
get_filename_component(INPUT_BASENAME "${INPUT_FILE}" NAME)
set(PREVIOUS_FILE "${CMAKE_CURRENT_BINARY_DIR}/${INPUT_BASENAME}.prev")
execute_process(COMMAND "${CMAKE_COMMAND}" -E compare_files "${INPUT_FILE}" "${PREVIOUS_FILE}"
RESULT_VARIABLE difference OUTPUT_QUIET ERROR_QUIET)
# Files are the same, leave this function
if (difference EQUAL 0)
set(${VARIABLE} FALSE PARENT_SCOPE)
return()
endif()
set(${VARIABLE} TRUE PARENT_SCOPE)
# Update the file with the latest
if (EXISTS "${INPUT_FILE}")
execute_process(COMMAND "${CMAKE_COMMAND}" -E copy "${INPUT_FILE}" "${PREVIOUS_FILE}" OUTPUT_QUIET)
endif()
endfunction()
####
# read_from_lines:
#
# Reads a set of variables from a newline delimited test base. This will read each variable as a separate line. It is
# based on the number of arguments passed in.
####
function (read_from_lines CONTENT)
# Loop through each arg
foreach(NAME IN LISTS ARGN)
string(REGEX MATCH "^([^\r\n]+)" VALUE "${CONTENT}")
string(REGEX REPLACE "^([^\r\n]*)\r?\n(.*)" "\\2" CONTENT "${CONTENT}")
set(${NAME} "${VALUE}" PARENT_SCOPE)
endforeach()
endfunction()
####
# Function `full_path_from_build_relative_path`:
#
# Creates a full path from the shortened build-relative path.
# -**SHORT_PATH:** build relative path
# Return: full path from relative path
####
function(full_path_from_build_relative_path SHORT_PATH OUTPUT_VARIABLE)
foreach(FPRIME_LOCATION IN LISTS FPRIME_BUILD_LOCATIONS)
if (EXISTS "${FPRIME_LOCATION}/${SHORT_PATH}")
set("${OUTPUT_VARIABLE}" "${FPRIME_LOCATION}/${SHORT_PATH}" PARENT_SCOPE)
return()
endif()
endforeach()
set("${OUTPUT_VARIABLE}" "" PARENT_SCOPE)
endfunction(full_path_from_build_relative_path)
####
# Function `get_nearest_build_root`:
#
# Finds the nearest build root from ${FPRIME_BUILD_LOCATIONS} that is a parent of DIRECTORY_PATH.
#
# - **DIRECTORY_PATH:** path to detect nearest build root
# Return: nearest parent from ${FPRIME_BUILD_LOCATIONS}
####
function(get_nearest_build_root DIRECTORY_PATH)
get_filename_component(DIRECTORY_PATH "${DIRECTORY_PATH}" ABSOLUTE)
set(FOUND_BUILD_ROOT "${DIRECTORY_PATH}")
set(LAST_REL "${DIRECTORY_PATH}")
foreach(FPRIME_BUILD_LOC ${FPRIME_BUILD_LOCATIONS} ${CMAKE_BINARY_DIR}/F-Prime ${CMAKE_BINARY_DIR})
get_filename_component(FPRIME_BUILD_LOC "${FPRIME_BUILD_LOC}" ABSOLUTE)
file(RELATIVE_PATH TEMP_MODULE ${FPRIME_BUILD_LOC} ${DIRECTORY_PATH})
string(LENGTH "${LAST_REL}" LEN1)
string(LENGTH "${TEMP_MODULE}" LEN2)
if (LEN2 LESS LEN1 AND TEMP_MODULE MATCHES "^[^./].*")
set(FOUND_BUILD_ROOT "${FPRIME_BUILD_LOC}")
set(LAST_REL "${TEMP_MODULE}")
endif()
endforeach()
if ("${FOUND_BUILD_ROOT}" STREQUAL "${DIRECTORY_PATH}")
message(FATAL_ERROR "No build root found for: ${DIRECTORY_PATH}")
endif()
set(FPRIME_CLOSEST_BUILD_ROOT "${FOUND_BUILD_ROOT}" PARENT_SCOPE)
endfunction()
####
# Function `get_module_name`:
#
# Takes a path, or something path-like and returns the module's name. This breaks down as the
# following:
#
# 1. If passed a path, the module name is the '_'ed variant of the relative path from BUILD_ROOT
# 2. If passes something which does not exist on the file system, it is just '_'ed
#
# i.e. ${BUILD_ROOT}/Svc/EventManager becomes Svc_EventManager
# Svc/EventManager also becomes Svc_EventManager
#
# - **DIRECTORY_PATH:** (optional) path to infer MODULE_NAME from. Default: CMAKE_CURRENT_LIST_DIR
# - **Return: MODULE_NAME** (set in parent scope)
####
function(get_module_name)
# Set optional arguments
if (ARGN)
set(DIRECTORY_PATH "${ARGN}")
else()
set(DIRECTORY_PATH "${CMAKE_CURRENT_LIST_DIR}")
endif()
resolve_path_variables(DIRECTORY_PATH)
# If DIRECTORY_PATH exists, then find its offset from BUILD_ROOT to calculate the module
# name. If it does not exist, then it is assumed to be an offset already and is carried
# forward in the calculation.
if (EXISTS ${DIRECTORY_PATH} AND IS_ABSOLUTE ${DIRECTORY_PATH})
# Module names a based on the current directory, not a file
if (NOT IS_DIRECTORY ${DIRECTORY_PATH})
get_filename_component(DIRECTORY_PATH "${DIRECTORY_PATH}" DIRECTORY)
endif()
# Get path name relative to the root directory
get_nearest_build_root(${DIRECTORY_PATH})
File(RELATIVE_PATH TEMP_MODULE_NAME ${FPRIME_CLOSEST_BUILD_ROOT} ${DIRECTORY_PATH})
else()
set(TEMP_MODULE_NAME ${DIRECTORY_PATH})
endif()
# Replace slash with underscore to have valid name
string(REPLACE "/" "_" TEMP_MODULE_NAME ${TEMP_MODULE_NAME})
set(MODULE_NAME ${TEMP_MODULE_NAME} PARENT_SCOPE)
endfunction(get_module_name)
####
# Function `get_expected_tool_version`:
#
# Gets the expected tool version named using version identifier VID to name the tools package
# file. This will be returned via the variable supplied in FILL_VARIABLE setting it in PARENT_SCOPE.
####
function(get_expected_tool_version VID FILL_VARIABLE)
find_program(TOOLS_CHECK NAMES fprime-version-check REQUIRED)
# Try project root as a source
set(REQUIREMENT_FILE "${FPRIME_PROJECT_ROOT}/requirements.txt")
if (EXISTS "${REQUIREMENT_FILE}")
execute_process(COMMAND "${TOOLS_CHECK}" "${VID}" "${REQUIREMENT_FILE}" OUTPUT_VARIABLE VERSION_TEXT ERROR_VARIABLE ERRORS RESULT_VARIABLE RESULT_OUT OUTPUT_STRIP_TRAILING_WHITESPACE)
fprime_cmake_debug_message("[VERSION] Could not detect version from: ${REQUIREMENT_FILE}. ${ERRORS}")
if (RESULT_OUT EQUAL 0)
set("${FILL_VARIABLE}" "${VERSION_TEXT}" PARENT_SCOPE)
return()
endif()
endif()
# Fallback to requirements.txt in fprime
set(REQUIREMENT_FILE "${FPRIME_FRAMEWORK_PATH}/requirements.txt")
execute_process(COMMAND "${TOOLS_CHECK}" "${VID}" "${REQUIREMENT_FILE}" OUTPUT_VARIABLE VERSION_TEXT ERROR_VARIABLE ERRORS RESULT_VARIABLE RESULT_OUT OUTPUT_STRIP_TRAILING_WHITESPACE)
if (RESULT_OUT EQUAL 0)
set("${FILL_VARIABLE}" "${VERSION_TEXT}" PARENT_SCOPE)
return()
endif()
fprime_cmake_warning("[VERSION] Could not detect version from: ${REQUIREMENT_FILE}. ${ERRORS}. Skipping check.")
set("${FILL_VARIABLE}" "" PARENT_SCOPE)
endfunction(get_expected_tool_version)
####
# Function `set_assert_flags`:
#
# Adds a -DASSERT_FILE_ID=(First 8 digits of MD5) to each source file, and records the output in
# hashes.txt. This allows for asserts on file ID not string. Also adds the -DASSERT_RELATIVE_PATH
# flag for handling relative path asserts.
####
function(set_assert_flags SRC)
if (NOT SRC MATCHES "^[$].*") # skip if generator expression
get_nearest_build_root("${SRC}") # sets FPRIME_CLOSEST_BUILD_ROOT in current scope
endif()
get_filename_component(FPRIME_CLOSEST_BUILD_ROOT_ABS "${FPRIME_CLOSEST_BUILD_ROOT}" ABSOLUTE)
get_filename_component(FPRIME_PROJECT_ROOT_ABS "${FPRIME_PROJECT_ROOT}" ABSOLUTE)
string(REPLACE "${FPRIME_CLOSEST_BUILD_ROOT_ABS}/" "" SHORT_SRC "${SRC}")
string(REPLACE "${FPRIME_PROJECT_ROOT_ABS}/" "" SHORT_SRC "${SHORT_SRC}")
string(MD5 HASH_VAL "${SHORT_SRC}")
string(SUBSTRING "${HASH_VAL}" 0 8 HASH_32)
file(APPEND "${CMAKE_BINARY_DIR}/hashes.txt" "${SHORT_SRC}: 0x${HASH_32}\n")
SET_SOURCE_FILES_PROPERTIES(${SRC} PROPERTIES COMPILE_FLAGS "-DASSERT_FILE_ID=0x${HASH_32} -DASSERT_RELATIVE_PATH='\"${SHORT_SRC}\"'")
endfunction(set_assert_flags)
####
# Function `print_property`:
#
# Prints a given property for the module.
# - **TARGET**: target to print properties
# - **PROPERTY**: name of property to print
####
function (print_property TARGET PROPERTY)
get_target_property(OUT "${TARGET}" "${PROPERTY}")
if (NOT OUT MATCHES ".*-NOTFOUND")
fprime_cmake_status("[F´ Module] ${TARGET} ${PROPERTY}:")
foreach (PROPERTY IN LISTS OUT)
fprime_cmake_status("[F´ Module] ${PROPERTY}")
endforeach()
endif()
endfunction(print_property)
####
# Function `introspect`:
#
# Prints the dependency list of the module supplied as well as the include directories.
#
# - **MODULE_NAME**: module name to print dependencies
####
function(introspect MODULE_NAME)
print_property("${MODULE_NAME}" SOURCES)
print_property("${MODULE_NAME}" SUPPLIED_HEADERS)
print_property("${MODULE_NAME}" INCLUDE_DIRECTORIES)
print_property("${MODULE_NAME}" LINK_LIBRARIES)
print_property("${MODULE_NAME}" INTERFACE_LINK_LIBRARIES)
endfunction(introspect)
####
# Function `execute_process_or_fail`:
#
# Calls CMake's `execute_process` with the arguments passed in via ARGN. This call is wrapped to print out the command
# line invocation when CMAKE_DEBUG_OUTPUT is set ON, and will check that the command processes correctly. Any error
# message is output should the command fail. No handling is done of standard error.
#
# Errors are determined by checking the process's return code where a FATAL_ERROR is produced on non-zero.
#
# - **ERROR_MESSAGE**: message to output should an error occurs
####
function(execute_process_or_fail ERROR_MESSAGE)
# Quiet standard output unless we are doing verbose output (handled below)
set(OUTPUT_ARGS OUTPUT_QUIET)
# Print the invocation if debug output is set
set(OUTPUT_ARGS)
set(COMMAND_AS_STRING "")
foreach(ARG IN LISTS ARGN)
set(COMMAND_AS_STRING "${COMMAND_AS_STRING}\"${ARG}\" ")
endforeach()
fprime_cmake_debug_message("[cli] ${COMMAND_AS_STRING}")
# Ninja pipes stderr to stdout so remove quiet output to see errors
if (CMAKE_GENERATOR MATCHES "Ninja")
set(OUTPUT_ARGS)
endif()
execute_process(
COMMAND ${ARGN}
RESULT_VARIABLE RETURN_CODE
OUTPUT_VARIABLE STANDARD_OUTPUT
ERROR_VARIABLE STANDARD_ERROR
ERROR_STRIP_TRAILING_WHITESPACE
OUTPUT_STRIP_TRAILING_WHITESPACE
${OUTPUT_ARGS}
)
if (NOT RETURN_CODE EQUAL 0)
set(FATAL_MESSAGE "${ERROR_MESSAGE}:\n${STANDARD_ERROR}")
# Ninja pipes stderr to stdout so we have to print stdout to see errors
if (CMAKE_GENERATOR MATCHES "Ninja")
string(APPEND FATAL_MESSAGE "\n${STANDARD_OUTPUT}")
endif()
message(FATAL_ERROR "${FATAL_MESSAGE}")
endif()
endfunction()
####
# Function `append_list_property`:
#
# Appends the NEW_ITEM to a property. ARGN is a set of arguments that are passed into the get and set property calls.
# This function calls get_property with ARGN appends NEW_ITEM to the result and then turns around and calls set_property
# with the new list. Callers **should not** supply the variable name argument to get_property.
#
# Duplicate entries are removed.
#
# Args:
# - `NEW_ITEM`: item to append to the property
# - `ARGN`: list of arguments forwarded to get and set property calls.
####
function(append_list_property NEW_ITEM)
get_property(LOCAL_COPY ${ARGN})
list(APPEND LOCAL_COPY ${NEW_ITEM})
list(REMOVE_DUPLICATES LOCAL_COPY)
set_property(${ARGN} "${LOCAL_COPY}")
endfunction()
####
# Function `filter_lists`:
#
# Filters lists set in ARGN to to ensure that they are not in the exclude list. Sets the <LIST>_FILTERED variable in
# PARENT_SCOPE with the results
# **EXCLUDE_LIST**: list of items to filter-out of ARGN lists
# **ARGN:** list of list names in parent scope to filter
####
function (filter_lists EXCLUDE_LIST)
foreach(SOURCE_LIST IN LISTS ARGN)
set(${SOURCE_LIST}_FILTERED "")
foreach(SOURCE IN LISTS ${SOURCE_LIST})
if (NOT SOURCE IN_LIST EXCLUDE_LIST)
list(APPEND ${SOURCE_LIST}_FILTERED "${SOURCE}")
endif()
endforeach()
set(${SOURCE_LIST}_FILTERED "${${SOURCE_LIST}_FILTERED}" PARENT_SCOPE)
endforeach()
endfunction(filter_lists)
####
# Function `get_fprime_library_option_string`:
#
# Returns a standard library option string from a name. Library option strings are derived from the directory and
# converted to a set of valid characters: [A-Z0-9_]. Alphabetic characters are made uppercase, numeric characters are
# maintained, and other characters are replaced with _.
#
# If multiple directories convert to the same name, these are effectively merged with respect to library options.
#
# OUTPUT_VAR: output variable to be set in parent scope
# LIBRARY_NAME: library name to convert to option
####
function(get_fprime_library_option_string OUTPUT_VAR LIBRARY_NAME)
string(TOUPPER "${LIBRARY_NAME}" LIBRARY_NAME_UPPER)
string(REGEX REPLACE "[^A-Z0-9_]" "_" LIBRARY_OPTION "${LIBRARY_NAME_UPPER}")
set("${OUTPUT_VAR}" "${LIBRARY_OPTION}" PARENT_SCOPE)
endfunction(get_fprime_library_option_string)
####
# Function `resolve_path_variables`:
#
# Resolve paths updating parent scope. ARGN should contain a list of variables to update.
#
# ARGN: list of variables to update
####
function(resolve_path_variables)
# Loop through all variables
foreach (INPUT_NAME IN LISTS ARGN)
set(NEW_LIST)
# Loop through each item in INPUT_NAME
foreach(UNRESOLVED IN LISTS ${INPUT_NAME})
get_filename_component(ABSOLUTE_UNRESOLVED "${UNRESOLVED}" ABSOLUTE)
# If it is a path, resolve it
if (EXISTS ${ABSOLUTE_UNRESOLVED})
get_filename_component(RESOLVED "${ABSOLUTE_UNRESOLVED}" REALPATH)
else()
set(RESOLVED "${UNRESOLVED}")
endif()
list(APPEND NEW_LIST "${RESOLVED}")
endforeach()
set("${INPUT_NAME}" "${NEW_LIST}" PARENT_SCOPE)
endforeach()
endfunction(resolve_path_variables)
####
# Function `fprime_cmake_fatal_error`:
#
# Prints a fatal error message to the user, highlighted with ---- to make it obvious. For multi-line
# messages, place a \n at the end of the previous message.
#
# - **ARGN**: message(s) to print separated by ' 's
####
function(fprime_cmake_fatal_error)
fprime_cmake_clear_message(FATAL_ERROR ${ARGN})
endfunction(fprime_cmake_fatal_error)
####
# Function `fprime_cmake_warning`:
#
# Prints a warning message to the user, highlighted with ---- to make it obvious. For multi-line
# messages, place a \n at the end of the previous message.
#
# - **ARGN**: message(s) to print separated by ' 's
####
function(fprime_cmake_warning)
fprime_cmake_clear_message(WARNING ${ARGN})
endfunction(fprime_cmake_warning)
####
# Function `fprime_cmake_status`:
#
# Prints a status message to the user that can be quieted with FPRIME_CMAKE_QUIET=ON.
# - **ARGN**: arguments to CMake's message() function w/o severity level
####
function(fprime_cmake_status)
if (NOT FPRIME_CMAKE_QUIET)
message(STATUS ${ARGN})
endif()
endfunction(fprime_cmake_status)
####
# Function `fprime_cmake_debug_message`:
#
# Prints a debug message.
#
# - **MESSAGE**: message to print
####
function(fprime_cmake_debug_message MESSAGE)
if (CMAKE_DEBUG_OUTPUT)
message(STATUS " [DEBUG] ${MESSAGE}")
endif()
endfunction(fprime_cmake_debug_message)
####
# Function `fprime__cmake_clear_message`:
#
# Prints a message to the user, highlighted with ---- to make it obvious and including the list file
# that is failing. For multi-line messages, place a \n at the end of the previous message.
#
# - **SEVERITY**: message severity to use
# - **ARGN**: message(s) to print separated by ' 's
####
function(fprime_cmake_clear_message SEVERITY)
string(REPLACE ";" " " MESSAGE "${ARGN}")
message("${SEVERITY}" " ----------------------------------------\n"
" ${MESSAGE} in:\n"
" ${CMAKE_CURRENT_LIST_FILE}\n"
" ----------------------------------------\n")
endfunction()
####
# Macro `fprime_cmake_ASSERT`:
#
# Checks condition, prints message. This is a macro so the condition is pasted into the message as well as
# the conditional clause.
#
# - **CONDITION**: condition to evaluate with if (${CONDITION})
####
macro(fprime_cmake_ASSERT MESSAGE)
# Simplify the evaluation of the condition by not placing NOT in front. Just have a no-op if clause
# where the else prints the FATAL message.
if (${ARGN})
else ()
string(REPLACE ";" " " FPRIME_INTERNAL_STRING_FROM_ARGN "${ARGN}")
message(FATAL_ERROR " ----------------------------------------\n"
" Assertion (${FPRIME_INTERNAL_STRING_FROM_ARGN}) failed with message '${MESSAGE}'. In:\n"
" ${CMAKE_CURRENT_FUNCTION_LIST_FILE}:${CMAKE_CURRENT_FUNCTION_LIST_LINE}\n"
" ----------------------------------------\n")
endif()
endmacro()
####
# Function `recurse_target_properties`:
#
# Recurses the supplied PROPERTY_NAMES of the CMAKE_BUILD_TARGET_NAME target. Sets three variables TRANSITIVE_LINKS_OUTPUT, EXTERNAL_LINKS_OUTPUT,
# and NON_EXISTENT_LINKS_OUTPUT. Where TRANSITIVE_LINKS_OUTPUT holds the transitive values of target/links found in those properties (recursively),
# EXTERNAL_LINKS_OUTPUT holds IMPORTED type targets found in the recursion, and NON_EXISTENT_LINKS_OUTPUT holds unknown/non-target values found
# (recursively).
#
# NON_EXISTENT_LINKS_OUTPUT will include directly linked files, linker flags, and other non-target values.
#
# > [!WARNING]
# > Properties supplied through PROPERTY_NAMES must be composed of mostly target names (e.g. LINK_LIBRARIES, MANUALLY_ADDED_DEPENDENCIES, etc.)
#
# - **CMAKE_BUILD_TARGET_NAME**: name of the target in the CMake system
# - **PROPERTY_NAMES**: list of properties containing other CMake target names to be read recursively
# - **TRANSITIVE_LINKS_OUTPUT**: name of output to write transitive links/dependencies in PARENT_SCOPE
# - **EXTERNAL_LINKS_OUTPUT**: name of output to write external (IMPORTED) links/dependencies in PARENT_SCOPE
# - **NON_EXISTENT_LINKS_OUTPUT**: name of output to write non-target links/dependencies in PARENT_SCOPE
####
function(recurse_target_properties CMAKE_BUILD_TARGET_NAME PROPERTY_NAMES TRANSITIVE_LINKS_OUTPUT EXTERNAL_LINKS_OUTPUT NON_EXISTENT_LINKS_OUTPUT)
# Recursive leafs:
# 1. This is not a known target
# 2. This target has not further links
# If the current item is not a target, tell the parent that this is a nonexistent entity
if (NOT TARGET "${CMAKE_BUILD_TARGET_NAME}")
set("${NON_EXISTENT_LINKS_OUTPUT}" "${CMAKE_BUILD_TARGET_NAME}" PARENT_SCOPE)
set("${TRANSITIVE_LINKS_OUTPUT}" PARENT_SCOPE)
set("${EXTERNAL_LINKS_OUTPUT}" PARENT_SCOPE)
return()
endif()
# If the target is imported, tell the parent that this is an external target
get_target_property(IMPORTED_TARGET "${CMAKE_BUILD_TARGET_NAME}" IMPORTED)
if (IMPORTED_TARGET)
set("${NON_EXISTENT_LINKS_OUTPUT}" PARENT_SCOPE)
set("${TRANSITIVE_LINKS_OUTPUT}" PARENT_SCOPE)
set("${EXTERNAL_LINKS_OUTPUT}" "${CMAKE_BUILD_TARGET_NAME}" PARENT_SCOPE)
return()
endif()
# Read all supplied properties and add them to the list of items to recurse
set(PROPERTY_LIST)
foreach(PROPERTY_NAME IN LISTS PROPERTY_NAMES)
get_target_property(PROPERTY_LIST_LOOPED "${CMAKE_BUILD_TARGET_NAME}" "${PROPERTY_NAME}")
if (PROPERTY_LIST_LOOPED)
list(APPEND PROPERTY_LIST ${PROPERTY_LIST_LOOPED})
endif()
endforeach()
list(REMOVE_DUPLICATES PROPERTY_LIST)
# When there are no other link libraries below this one, return current target as the singular dependency
if (NOT PROPERTY_LIST)
set("${NON_EXISTENT_LINKS_OUTPUT}" PARENT_SCOPE)
set("${TRANSITIVE_LINKS_OUTPUT}" "${CMAKE_BUILD_TARGET_NAME}" PARENT_SCOPE)
set("${EXTERNAL_LINKS_OUTPUT}" PARENT_SCOPE)
return()
endif()
set(PREVIOUSLY_RECURSED ${ARGN} ${CMAKE_BUILD_TARGET_NAME})
# Look through each current link library using a recursive call
set(RECURSED_TRANSITIVE)
set(RECURSED_UNKNOWN)
set(RECURSED_EXTERNAL)
foreach(LINK IN LISTS PROPERTY_LIST)
unset(INTERNAL_TRANSITIVE)
unset(INTERNAL_UNKNOWN)
# Prevent redundant recursion
if (NOT LINK IN_LIST PREVIOUSLY_RECURSED AND NOT LINK STREQUAL "")
fprime_cmake_ASSERT("'${LINK}' is a null dependency of '${CMAKE_BUILD_TARGET_NAME}'" LINK)
# Recurse through each link and append the recursively determined additions to the list
# while ensuring there are no duplicates
recurse_target_properties("${LINK}" "${PROPERTY_NAMES}" INTERNAL_TRANSITIVE INTERNAL_EXTERNAL INTERNAL_UNKNOWN ${PREVIOUSLY_RECURSED})
# The current link must occur in one list or the other
fprime_cmake_ASSERT("'${LINK}' must appear in '${INTERNAL_TRANSITIVE}' or '${INTERNAL_UNKNOWN}'"
LINK IN_LIST INTERNAL_TRANSITIVE OR LINK IN_LIST INTERNAL_UNKNOWN OR LINK IN_LIST INTERNAL_EXTERNAL)
# Append the lists to the aggregated output
list(APPEND RECURSED_TRANSITIVE ${INTERNAL_TRANSITIVE})
list(APPEND RECURSED_UNKNOWN ${INTERNAL_UNKNOWN})
list(APPEND RECURSED_EXTERNAL ${INTERNAL_EXTERNAL})
list(REMOVE_DUPLICATES RECURSED_TRANSITIVE)
list(REMOVE_DUPLICATES RECURSED_UNKNOWN)
list(REMOVE_DUPLICATES RECURSED_EXTERNAL)
# Update previously touched modules
list(APPEND PREVIOUSLY_RECURSED ${INTERNAL_TRANSITIVE} ${INTERNAL_UNKNOWN} ${INTERNAL_EXTERNAL})
list(REMOVE_DUPLICATES PREVIOUSLY_RECURSED)
endif()
endforeach()
# Return the results of this stage of the recursion
set("${NON_EXISTENT_LINKS_OUTPUT}" ${RECURSED_UNKNOWN} PARENT_SCOPE)
set("${TRANSITIVE_LINKS_OUTPUT}" ${CMAKE_BUILD_TARGET_NAME} ${RECURSED_TRANSITIVE} PARENT_SCOPE)
set("${EXTERNAL_LINKS_OUTPUT}" ${RECURSED_EXTERNAL} PARENT_SCOPE)
endfunction()
####
# Function `fprime__internal_target_interceptor`:
#
# A function that intercepts calls to target_* functions and translates the scope from PUBLIC to INTERFACE when the
# target is an INTERFACE target.
#
# - **FUNCTION_NAME**: name of the target_* function to intercept
# - **BUILD_TARGET_NAME**: name of the target to set
# - **SCOPE**: scope of the target to intercept and change
# - **ARGN**: arguments to pass to the target_* function
####
function(fprime__internal_target_interceptor FUNCTION_NAME BUILD_TARGET_NAME SCOPE)
# Get the target type
get_target_property(TARGET_TYPE "${BUILD_TARGET_NAME}" TYPE)
# If the target is an INTERFACE_LIBRARY, change the scope to INTERFACE
if (TARGET_TYPE STREQUAL "INTERFACE_LIBRARY" AND SCOPE STREQUAL "PUBLIC")
set(SCOPE INTERFACE)
endif()
# Call the target_* function with the new scope
cmake_language(CALL "${FUNCTION_NAME}" "${BUILD_TARGET_NAME}" "${SCOPE}" ${ARGN})
endfunction()
####
# Function `fprime_target_link_libraries`:
#
# This function wraps `target_link_libraries` to ensure that PUBLIC scope additions translate to INTERFACE when
# the target is an INTERFACE target. This makes it easier to deal with INTERFACE targets.
#
# See: target_link_libraries
####
function(fprime_target_link_libraries BUILD_TARGET_NAME SCOPE)
fprime__internal_target_interceptor("target_link_libraries" "${BUILD_TARGET_NAME}" "${SCOPE}" ${ARGN})
endfunction()
####
# Function `fprime_target_include_directories`:
#
# This function wraps `target_include_directories` to ensure that PUBLIC scope additions translate to INTERFACE when
# the target is an INTERFACE target. This makes it easier to deal with INTERFACE targets.
#
# See: target_include_directories
#
####
function(fprime_target_include_directories BUILD_TARGET_NAME SCOPE)
fprime__internal_target_interceptor("target_include_directories" "${BUILD_TARGET_NAME}" "${SCOPE}" ${ARGN})
endfunction()
####
# Function `fprime_target_dependencies`:
#
# Adds dependencies to the supplied BUILD_TARGET_NAME properly handling scope (see fprime_target_link_libraries). Adding a dependency
# involves 2 steps:
# 1. Adding a link dependency from BUILD_TARGET_NAME to supplied dependencies
# 2. Append supplied dependencies to the FPRIME_DEPENDENCIES property of BUILD_TARGET_NAME
#
# - **BUILD_TARGET_NAME**: name of the target to add dependencies to
# - **SCOPE**: scope of the target to intercept and change from PUBLIC to INTERFACE for INTERFACE_LIBRARY targets targets
# - **ARGN**: dependencies to add to the target
####
function(fprime_target_dependencies BUILD_TARGET_NAME SCOPE)
fprime_target_link_libraries("${BUILD_TARGET_NAME}" "${SCOPE}" ${ARGN})
append_list_property("${ARGN}" TARGET "${BUILD_TARGET_NAME}" PROPERTY FPRIME_DEPENDENCIES)
endfunction()