CMake utilities

This is a CMake package that provides functionality of general interest, such as logging, test assertions, and key to value mapping.

Dynamic functions

This module implements several meta-functions that create new functions dynamically by wrapping the existing ones.

The function parameter_to_function_prefix wraps a function f(x1 x2 …) into a new function ${x1}_f(x2 …), that is, fixes one argument to some predefined value so that the wrapped function could be called with one argument less.

The function trace_functions wraps a function f into a new function _trace_f that prints a trace message f(${ARGN}) into the log context with the same name f, and then calls the original function with unchanged arguments.

The function dynamic_call forwards given arguments to a given function without any changes.

The function eval emulates return values for functions and macros:

function(concat_function _param1 _param2)
    result_is("${_param1}_${_param2}")
endfunction()

set(_ind 2)
eval(EXPR "a = ${_ind} + 3")
message(STATUS "a = ${a}") # prints a = 5

eval("b = concat_function(cmake language)")
message(STATUS "b = ${b}") # prints b = cmake_language

When to use

With the help of trace_functions, it’s easy to single-out calls to a specific function without changing the source of that function. parameter_to_function_prefix is written specifically to wrap the GlobalMap module, but it could be useful in other contexts as well.

Functions

parameter_to_function_prefix
parameter_to_function_prefix(<function list>>)

For each given function fn, creates a counterpart ${_prefix}_${fn} that has all the parameters of the original function except the first one, and does nothing but calls fn with the arguments given to ${_prefix}_${fn}.

Note

Since the argument count changes, the logic that depends on ARGN, ARGC and similar values, may break. Be careful when wrapping a function that depends on exact argument count and order.

trace_functions
trace_functions(<function list>>)

For each given function fn, creates a counterpart _trace_fn that prints a trace message called fn(${ARGN}) into the log context fn and then calls the original function with the unchanged arguments. For example:

trace_functions(global_set)
log_level(global_set TRACE)
log_to_file(global_set global_set.log)
# will append 'global_set(a bcd)' to 'global_set.log'
# before calling 'global_set'
_trace_global_set(a bcd)
dynamic_call
dynamic_call(<function> <argument list>)

Calls the given function with the given arguments. For example:

log_level(dynamic_call TRACE)
log_to_file(dynamic_call dynamic_call.log)
dynamic_call(${function_name} ${ARGN})
# will append 'global_set(a bcd)' to 'global_set.log'
# before calling 'global_set'
_trace_global_set(a bcd)
eval
eval("<var> = <function>(<argument>...)")

Calls the given function with the given arguments and stores the result in the result variable var. The function function must call one of result_is, result_expr to publish the result:

function(function_with_result _param1 _param2)
   result_expr(${_param1} + ${_param2})
endfunction()

function(function_with_result_2 _param1 _param2)
   result_is("${_param1}_${_param2}")
endfunction()

macro(macro_with_result _param1 _param2)
    result_is("${_param1}_${_param2}")
endmacro()

function(eval_test)
   eval("a = function_with_result(3 4)")
   assert_same(${a} 7)
   eval("b = function_with_result_2(cmake language)")
   assert_same(${b} cmake_language)
   set(_ind 2)
   eval(EXPR "a = ${_ind} + 3")
   assert_same(${a} 5)
   eval("a = macro_with_result(1 2)")
   assert_same(${a} 1_2)
endfunction()

Global maps

A global map models an association between a name and one or more [key, value] pairs. In the implementation, these pairs are saved by the calls to

set_property(GLOBAL PROPERTY ${prefix}${key} ${value})

A prefix usually identifies a program context, so that different global maps separate different contexts. A global map maintains an index of the keys it stores, which can be used to find out whether a property is in the map or not. A map can also be cleared with the help of its index. It’s possible to set, unset, or append to a property using syntax similar to that of usual variables:

# set(variable value)
global_set(context variable value)
# get_property(GLOBAL PROPERTY variable value)
global_set(context variable value)
# unset(variable)
global_unset(context variable)
# list(APPEND variable value)
global_append(context variable value)

The first argument is always a map name; it limits the scope of the operation to a certain context. It’s convenient to think of this argument as of a map name, although in implementation it’s just a prefix of the stored keys.

When to use

Sometimes, a CMake function or a module can have a complex state. In such cases, writing something like

get_property(value GLOBAL PROPERTY property)
set_property(GLOBAL PROPERTY property ${value} ${additional_value})

just to append a value to an existing property becomes a tedious, error-prone task.

Write functions

global_set
global_set(map_name property value)

Stores the [property, value] pair in the global map map_name. The value can be retrieved later using read functions.

Example:

function(setup_test value)
    global_set(test conf_key ${value})
endfunction()

function(run_test)
    global_get(test conf_key value)
    message(STATUS "run the test with the conf_key = ${value}")
    # run the test ...
endfunction()

setup_test(conf_value)
# ...
run_test()
global_set_if_empty
global_set_if_empty(map_name property value)

If the global map map_name does not contain the key property, stores the [property, value] pair in that map. Otherwise, raises an error (SEND_ERROR) without updating the map.

Example:

foreach(key ${keys})
    # require key uniqueness
    global_set_if_empty(unique keys ${key})
endforeach()
global_append
global_append(map_name property value)

If the property property exists, it is treated as a list, and the value of value is appended to it. Otherwise, the property property is created and set to the given value.

Example:

# filter out the list into a new list for later use
function(filter_interface_targets)
   foreach(_target ${_targets})
       get_target_property(_type ${_target} TYPE)
       if (_type STREQUAL INTERFACE_LIBRARY)
           global_append(interface_targets ${_target})
       endif()
    endforeach()
endfunction()

add_library(target1 INTERFACE)
add_library(target2 INTERFACE)
add_library(target3 tests/test1.cpp)
filter_interface_targets(target1 target2 target3)
# ...
global_get(interface_targets targets)
# prints: target1;target2
print("INTERFACE targets: ${targets}")
global_unset
global_unset(map_name property)

Removes the property property from the global map map_name.

Example:

global_set(test key1 value1)
global_set(test key2 value2)
global_set(test key3 value3)
# ...
global_unset(test)
global_get(test key1 value)
assert_empty("${value}")
global_get(test key2 value)
assert_not_empty("${value}")
global_clear
global_clear(map_name)

Clears all the properties previously set in the global map map_name by the calls to global_set and global_append.

Example:

global_set(test key1 value1)
global_set(test key2 value2)
global_set(test key3 value3)
# ...
global_clear(test)
global_get(test key1 value)
assert_empty("${value}")
global_get(test key2 value)
assert_empty("${value}")
global_get(test key3 value)
assert_empty("${value}")

Read functions

global_get
global_get(map_name property out_var)

Stores the value of the property property into the output variable designated by out_var. If the requested property is not found, sets out_var to an empty string.

Example See the example for global_set.

global_get_or_fail
global_get_or_fail(map_name property out_var)

Searches the property property in the given global map map_name. If found, the output variable out_var is updated to store the property’s value. Otherwise, fatal error is raised.

Example

if(condition)
    unset(var)
endif()
global_set(test property ${var})
# this will raise the fatal error - condition was not expected to work
global_get_or_fail(test property value)

Logging

This module implements a set of functions for logging messages either to a console or to a file. Every message has a category that acts as a searchable hierarchical tag, and optional parameters that will be substituted into the message by replacing placeholders {1}, {2}, etc. Output have the following format:

[<timestamp>][<category>][<level>] <message after substitutions>

For example,

# [2021-10-05T23:15:08][test][INFO] The path 'examples' will be used
log_info(test "The path '{1}' will be used" examples)
# send log messages in `test` to the file `test.log`
log_to_file(test test.log)
# message appended to test.log
log_warn(test "ICU not found")
# enable debug messages
log_level(test DEBUG)
# printed as well - categories maintain parent-child relation
log_info(test.nested "This message should be logged, too.")

Notice the parameter examples after the main message in the example above. Use of parameters is entirely optional; they are only for readability.

When to use

Controlled logging is useful in debugging, when it’s easy to add a lot of calls to message and it’s hard to remove them afterwards. This is not needed with this module - just raise the logging level of the corresponding category with either a call to log_level or via

-Dlog.context.<<context name>>.level=ERROR

Functions

log_message
log_message(_level _category _message)

Formats and prints the given message. Filters out the messages based on the logging level of the category _category, previously specified by a call to log_level.

log_level
log_level(_category _level)

All the subsequent messages in the given category _category and its nested categories will only be printed if their level is at least as high as _level. The levels are defined by the function _log_levels (the further it is from the beginning, the higher it is). The default level for every category is WARN.

log_to_file
log_to_file(_category _file_name)

Redirects all subsequent logged messages in the category _category and its nested categories to a file _file_name instead of the console.

log_to_console
log_to_console(_category)

Directs all subsequent logged messages in the category _category to the console instead of a file, previously specified by a call to log_to_file. Does nothing if the message redirection was not requested for the given category.

log_debug
log_debug(_category _message <message arguments>)

Formats the given message and prints it either to a console or to a file. The messages have the following format: [<<timestamp>>][<<category>>][DEBUG] <<message after substitutions>>> This function is a wrapper around log_message.

log_trace
log_trace(_category _message <message arguments>)

Formats the given message and prints it either to a console or to a file. The messages have the following format: [<<timestamp>>][<<category>>][TRACE] <<message after substitutions>>> This function is a wrapper around log_message.

log_info
log_info(_category _message <message arguments>)

Calls log_message with the level set to INFO.

log_error
log_error(_category _message <message arguments>)

Calls log_message with the level set to ERROR.

log_warn
log_warn(_category _message <message arguments>)

Calls log_message with the level set to FATAL.

log_warn
log_warn(_category _message <message arguments>)

Calls log_message with the level set to WARN.

Test assertions

This module implements a set of assertions for CMake-based tests.

Functions

assert_not_empty
assert_not_empty(value)

If the string given by value is empty, emits an error message. Does nothing otherwise.

assert_empty
assert_empty(value)

If the string given by value is not empty, emits an error message. Does nothing otherwise.

assert_same
assert_same(value)

If the strings str1 and str2 are not equal, emits an error message. Does nothing otherwise.

assert_ends_with
assert_ends_with(value)

If the string str1 does not end with str2, emits an error message. Does nothing otherwise.

assert
assert(value)

If value evaluates to false, emits an error message. Does nothing otherwise.

assert
assert_list_contains(_list _el)

If the list _list does not contain the element _el, emits an error message. Does nothing otherwise.

Change log

  • 2021-10-07 Added experimental support for colorized logging under Win32

License

MIT License

Copyright (c) 2021 Igor Chalenko

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Indices and tables