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.