docs: Add documentation on how to define executable targets in CMake

Includes the new `build_exec` macro.
This commit is contained in:
Gabriel Ferreira
2022-09-16 19:22:09 -03:00
parent c633a1420c
commit 24ee65fb23

View File

@@ -1489,6 +1489,184 @@ on user options, checking for dependencies of enabled features,
pre-compiling headers, filtering enabled/disabled modules and dependencies,
and more.
Executable macros
=================
Creating an executable in CMake requires a few different macro calls.
Some of these calls are related to setting the target and built executable name,
indicating which libraries that should be linked to the executable,
where the executable should be placed after being built and installed.
Note that if you are trying to add a new example to your module, you should
look at the `build_lib_example`_ macro section.
If you are trying to add a new example to ``~/ns-3-dev/examples``, you should
look at the `build_example`_ macro section.
While both of the previously mentioned macros are meant to be used for examples,
in some cases additional utilities are required. Those utilities can be helpers,
such as the ``raw-sock-creator`` in the ``fd-net-device`` module, or benchmark
tools in the ``~/ns-3-dev/utils`` directory. In those cases, the `build_exec`_
macro is recommended instead of direct CMake calls.
.. _build_exec:
Executable macros: build_exec
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``build_exec`` macro bundles a series of direct CMake calls into a single macro.
The example below shows the creation of an executable named ``example``, that will
later receive a version prefix (e.g. ``ns3.37-``) and a build type suffix
(e.g. ``-debug``), resulting in an executable file named ``ns3.37-example-debug``.
The list of source and header files can be passed in the ``SOURCE_FILES`` and
``HEADER_FILES`` arguments, followed by the ``LIBRARIES_TO_LINK`` that will be linked
to the executable.
That executable will be saved by default to the ``CMAKE_RUNTIME_OUTPUT_DIRECTORY``
(e.g. /ns-3-dev/build/bin). To change its destination, set ``EXECUTABLE_DIRECTORY_PATH``
to the desired path. The path is relative to the ``CMAKE_OUTPUT_DIRECTORY``
(e.g. /ns-3-dev/build).
In case this executable should be installed, set ``INSTALL_DIRECTORY_PATH`` to the
desired destination. In case this value is empty, the executable will not be installed.
The path is relative to the ``CMAKE_INSTALL_PREFIX`` (e.g. /usr).
To set custom compiler defines for that specific executable, defines can be passed
to the ``DEFINITIONS`` argument.
Finally, to ignore precompiled headers, include ``IGNORE_PCH`` to the list of parameters.
You can find more information about ``IGNORE_PCH`` at the `PCH side-effects`_ section.
.. sourcecode:: cmake
build_exec(
# necessary
EXECNAME example # executable name = example (plus version prefix and build type suffix)
SOURCE_FILES example.cc example-complement.cc
HEADER_FILES example.h
LIBRARIES_TO_LINK ${libcore} # links to core
EXECUTABLE_DIRECTORY_PATH scratch # build/scratch
# optional
EXECNAME_PREFIX scratch_subdir_prefix_ # target name = scratch_subdir_prefix_example
INSTALL_DIRECTORY_PATH ${CMAKE_INSTALL_BIN}/ # e.g. /usr/bin/ns3.37-scratch_subdir_prefix_example-debug
DEFINITIONS -DHAVE_FEATURE=1 # defines for this specific target
IGNORE_PCH
)
The same executable can be built by directly calling the following CMake macros:
.. sourcecode:: cmake
set(target_prefix scratch_subdir_prefix_)
set(target_name example)
set(output_directory scratch)
# Creates a target named "example" (target_name) prefixed with "scratch_subdir_prefix_" (target_prefix)
# e.g. scratch_subdir_prefix_example
add_executable(${target_prefix}${target_name} example.cc example-complement.cc)
target_link_libraries(${target_prefix}${target_name} PUBLIC ${libcore})
# Create a variable with the target name prefixed with
# the version and suffixed with the build profile suffix
# e.g. ns3.37-scratch_subdir_prefix_example-debug
set(ns3-exec-outputname ns${NS3_VER}-${target_prefix}${target_name}${build_profile_suffix})
# Append the binary name to the executables list later written to the lock file,
# which is consumed by the ns3 script and test.py
set(ns3-execs "${output_directory}${ns3-exec-outputname};${ns3-execs}"
CACHE INTERNAL "list of c++ executables"
)
# Modify the target properties to change the binary name to ns3-exec-outputname contents
# and modify its output directory (e.g. scratch). The output directory is relative to the build directory.
set_target_properties(
${target_prefix}${target_name}
PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${output_directory}
RUNTIME_OUTPUT_NAME ${ns3-exec-outputname}
)
# Create a dependency between the target and the all-test-targets
# (used by ctest, coverage and doxygen targets)
add_dependencies(all-test-targets ${target_prefix}${target_name})
# Create a dependency between the target and the timeTraceReport
# (used by Clang TimeTrace to collect compilation statistics)
add_dependencies(timeTraceReport ${target_prefix}${target_name}) # target used to track compilation time
# Set target-specific compile definitions
target_compile_definitions(${target_prefix}${target_name} PUBLIC definitions)
# Check whether the target should reuse or not the precompiled headers
if(NOT ${IGNORE_PCH})
target_precompile_headers(
${target_prefix}${target_name} REUSE_FROM stdlib_pch_exec
)
endif()
.. _build_example:
Executable macros: build_example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The ``build_example`` macro sets some of ``build_exec``'s arguments based on the current
example directory (output directory) and adds the optional visualizer module as a dependency
in case it is enabled. It also performs dependency checking on the libraries passed.
In case one of the dependencies listed is not found, the example target will not be created.
If you are trying to add an example or a dependency to an existing example and it is not
listed by ``./ns3 show targets`` or your IDE, check if all its dependencies were found.
.. sourcecode:: cmake
macro(build_example)
set(options IGNORE_PCH)
set(oneValueArgs NAME)
set(multiValueArgs SOURCE_FILES HEADER_FILES LIBRARIES_TO_LINK)
# Parse arguments
cmake_parse_arguments(
"EXAMPLE" "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}
)
# Check if any of the LIBRARIES_TO_LINK is missing to prevent configuration errors
check_for_missing_libraries(
missing_dependencies "${EXAMPLE_LIBRARIES_TO_LINK}"
)
if(NOT missing_dependencies)
# Convert boolean into text to forward argument
if(${EXAMPLE_IGNORE_PCH})
set(IGNORE_PCH IGNORE_PCH)
endif()
# Create example library with sources and headers
# cmake-format: off
build_exec(
EXECNAME ${EXAMPLE_NAME}
SOURCE_FILES ${EXAMPLE_SOURCE_FILES}
HEADER_FILES ${EXAMPLE_HEADER_FILES}
LIBRARIES_TO_LINK ${EXAMPLE_LIBRARIES_TO_LINK} ${optional_visualizer_lib}
EXECUTABLE_DIRECTORY_PATH
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/examples/${examplefolder}/
${IGNORE_PCH}
)
# cmake-format: on
endif()
endmacro()
An example on how it is used can be found in ``~/ns-3-dev/examples/tutorial/CMakeLists.txt``:
.. sourcecode:: cmake
build_example(
NAME first
SOURCE_FILES first.cc
LIBRARIES_TO_LINK
${libcore}
${libpoint-to-point}
${libinternet}
${libapplications}
# If visualizer is available, the macro will add the module to this list automatically
# build_exec's EXECUTABLE_DIRECTORY_PATH will be set to build/examples/tutorial/
)
Module macros
=============
@@ -1500,6 +1678,8 @@ for user scripts.
These macros are responsible for easing the porting of modules from Waf to CMake.
.. _build_lib:
Module macros: build_lib
~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1508,8 +1688,11 @@ block by block.
The first block declares the arguments received by the macro (in CMake, the
only difference is that a function has its own scope). Notice that there are
different types of arguments. Options that can only be set to true/false
(``IGNORE_PCH``).
different types of arguments. Options that can only be set to ON/OFF.
Options are OFF by default, and are set to ON if the option name is added to
the arguments list (e.g. ``build_lib(... IGNORE_PCH)``).
Note: You can find more information about ``IGNORE_PCH`` at the `PCH side-effects`_ section.
One value arguments that receive a single value
(usually a string) and in this case used to receive the module name (``LIBNAME``).
@@ -1891,6 +2074,9 @@ We also print an additional message the folder just finished being processed if
endif()
endfunction()
.. _build_lib_example:
Module macros: build_lib_example
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1924,6 +2110,7 @@ If the visualizer module is not enabled, ``optional_visualizer_lib`` is empty.
The example can also be linked to a single ns-3 shared library (``lib-ns3-monolib``) or
a single ns-3 static library (``lib-ns3-static``), if either ``NS3_MONOLIB=ON`` or ``NS3_STATIC=ON``.
Note that both of these options are handled by the ``build_exec`` macro.
.. sourcecode:: cmake
@@ -1931,55 +2118,34 @@ a single ns-3 static library (``lib-ns3-static``), if either ``NS3_MONOLIB=ON``
# ...
check_for_missing_libraries(missing_dependencies "${BLIB_EXAMPLE_LIBRARIES_TO_LINK}")
if(NOT missing_dependencies)
# Create shared library with sources and headers
add_executable(
"${BLIB_EXAMPLE_NAME}" ${BLIB_EXAMPLE_SOURCE_FILES}
${BLIB_EXAMPLE_HEADER_FILES}
)
if(${NS3_STATIC})
target_link_libraries(
${BLIB_EXAMPLE_NAME} ${LIB_AS_NEEDED_PRE_STATIC} ${lib-ns3-static}
)
elseif(${NS3_MONOLIB})
target_link_libraries(
${BLIB_EXAMPLE_NAME} ${LIB_AS_NEEDED_PRE} ${lib-ns3-monolib}
${LIB_AS_NEEDED_POST}
)
else()
target_link_libraries(
${BLIB_EXAMPLE_NAME} ${LIB_AS_NEEDED_PRE} ${lib${BLIB_EXAMPLE_LIBNAME}}
${BLIB_EXAMPLE_LIBRARIES_TO_LINK} ${optional_visualizer_lib}
${LIB_AS_NEEDED_POST}
)
endif()
# ...
# Convert boolean into text to forward argument
if(${BLIB_EXAMPLE_IGNORE_PCH})
set(IGNORE_PCH IGNORE_PCH)
endif()
# Create executable with sources and headers
# cmake-format: off
build_exec(
EXECNAME ${BLIB_EXAMPLE_NAME}
SOURCE_FILES ${BLIB_EXAMPLE_SOURCE_FILES}
HEADER_FILES ${BLIB_EXAMPLE_HEADER_FILES}
LIBRARIES_TO_LINK
${lib${BLIB_EXAMPLE_LIBNAME}} ${BLIB_EXAMPLE_LIBRARIES_TO_LINK}
${optional_visualizer_lib}
EXECUTABLE_DIRECTORY_PATH ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${FOLDER}/
${IGNORE_PCH}
)
# cmake-format: on
endif()
endfunction()
The `build_exec`_ macro will also set resulting folder where the example will end up
after built (e.g. build/src/module/examples). It does that by forwarding the
``EXECUTABLE_DIRECTORY_PATH`` to the macro ``set_runtime_outputdirectory``, which also
adds the proper ns-3 version prefix and build type suffix to the executable.
As with the module libraries, we can also reuse precompiled headers here to speed up
the parsing step of compilation.
Finally, we call another macro ``set_runtime_outputdirectory``, which indicates the
resulting folder where the example will end up after built (e.g. build/src/module/examples)
and adds the proper ns-3 version prefix and build type suffix to the executable.
.. sourcecode:: cmake
function(build_lib_example)
# ...
if(NOT missing_dependencies)
# ...
if(${PRECOMPILE_HEADERS_ENABLED} AND (NOT ${BLIB_EXAMPLE_IGNORE_PCH}))
target_precompile_headers(${BLIB_EXAMPLE_NAME} REUSE_FROM stdlib_pch_exec)
endif()
set_runtime_outputdirectory(
${BLIB_EXAMPLE_NAME}
${CMAKE_OUTPUT_DIRECTORY}/${FOLDER}/examples/ ""
)
endif()
endfunction()
You can find more information about ``IGNORE_PCH`` at the `PCH side-effects`_ section.
User options and header checking
================================
@@ -2398,6 +2564,9 @@ for each compilation unit (.cc file).
Note: for ease of use, PCH is enabled by default if supported. It can be manually disabled
by setting ``NS3_PRECOMPILE_HEADERS`` to ``OFF``.
Setting up and adding new headers to the PCH
++++++++++++++++++++++++++++++++++++++++++++
When both CCache and PCH are used together, there is a set of settings that must be
properly configured, otherwise timestamps built into the PCH can invalidate the CCache
artifacts, forcing a new build of unmodified modules/programs.
@@ -2520,3 +2689,97 @@ then cleaning, configuring, building, and finally printing the CCache statistics
If you have changed any compiler flag, the cache hit rate should be very low.
Repeat the same commands once more.
If the cache hit rate is at 100%, it means everything is working as it should.
.. _PCH side-effects:
Possible side-effects, fixes and IGNORE_PCH
+++++++++++++++++++++++++++++++++++++++++++
Precompiled headers can cause symbol collisions due to includes reordering or unwanted includes,
which can lead to attempts to redefine functions, macros, types or variables.
An example of such side-effect is shown below.
In order to exemplify how precompiled headers can cause issues, assume the following inclusion order from
``ns-3-dev/src/aodv/model/aodv-routing-protocol.cc``:
.. sourcecode:: cpp
...
#define NS_LOG_APPEND_CONTEXT \
if (m_ipv4) { std::clog << "[node " << m_ipv4->GetObject<Node> ()->GetId () << "] "; }
#include "aodv-routing-protocol.h"
#include "ns3/log.h"
...
The ``NS_LOG_APPEND_CONTEXT`` macro definition comes before the ``ns3/log.h`` inclusion,
and that is the expected way of using ``NS_LOG_APPEND_CONTEXT``, since we have the following
guards on ``ns3/log-macros-enabled.h``, which is included by ``ns3/logs.h`` when logs are enabled.
.. sourcecode:: cpp
...
#ifndef NS_LOG_APPEND_CONTEXT
#define NS_LOG_APPEND_CONTEXT
#endif /* NS_LOG_APPEND_CONTEXT */
...
By adding ``<ns3/logs.h>`` to the list of headers to precompile (``precompiled_header_libraries``)
in ``ns-3-dev/build-support/macros-and-definitions.cmake``, the ``ns3/logs.h`` header will
now be part of the PCH, which gets included before any parsing of the code is done.
This means the equivalent inclusion order would be different than what was originally intended,
as shown below:
.. sourcecode:: cpp
#include "cmake_pch.hxx" // PCH includes ns3/log.h before defining NS_LOG_APPEND_CONTEXT below
...
#define NS_LOG_APPEND_CONTEXT \
if (m_ipv4) { std::clog << "[node " << m_ipv4->GetObject<Node> ()->GetId () << "] "; }
#include "aodv-routing-protocol.h"
#include "ns3/log.h" // isn't processed since ``NS3_LOG_H`` was already defined by the PCH
...
While trying to build with the redefined symbols in the debug build, where warnings are treated
as errors, the build may fail with an error similar to the following from GCC 11.2:
.. sourcecode:: console
FAILED: src/aodv/CMakeFiles/libaodv-obj.dir/model/aodv-routing-protocol.cc.o
ccache /usr/bin/c++ ... -DNS3_LOG_ENABLE -Wall -Werror -include /ns-3-dev/cmake-build-debug/CMakeFiles/stdlib_pch.dir/cmake_pch.hxx
/ns-3-dev/src/aodv/model/aodv-routing-protocol.cc:28: error: "NS_LOG_APPEND_CONTEXT" redefined [-Werror]
28 | #define NS_LOG_APPEND_CONTEXT \
|
In file included from /ns-3-dev/src/core/model/log.h:32,
from /ns-3-dev/src/core/model/fatal-error.h:29,
from /ns-3-dev/build/include/ns3/assert.h:56,
from /ns-3-dev/build/include/ns3/buffer.h:26,
from /ns-3-dev/build/include/ns3/packet.h:24,
from /ns-3-dev/cmake-build-debug/CMakeFiles/stdlib_pch.dir/cmake_pch.hxx:23,
from <command-line>:
/ns-3-dev/src/core/model/log-macros-enabled.h:146: note: this is the location of the previous definition
146 | #define NS_LOG_APPEND_CONTEXT
|
cc1plus: all warnings being treated as errors
One of the ways to fix this issue in particular is undefining ``NS_LOG_APPEND_CONTEXT`` before redefining it in
``/ns-3-dev/src/aodv/model/aodv-routing-protocol.cc``.
.. sourcecode:: cpp
#include "cmake_pch.hxx" // PCH includes ns3/log.h before defining NS_LOG_APPEND_CONTEXT below
...
#undef NS_LOG_APPEND_CONTEXT // undefines symbol previously defined in the PCH
#define NS_LOG_APPEND_CONTEXT \
if (m_ipv4) { std::clog << "[node " << m_ipv4->GetObject<Node> ()->GetId () << "] "; }
#include "aodv-routing-protocol.h"
#include "ns3/log.h" // isn't processed since ``NS3_LOG_H`` was already defined by the PCH
...
If the ``IGNORE_PCH`` option is set in the `build_lib`_, `build_lib_example`_, `build_exec`_ and the `build_example`_ macros,
the PCH is not included in their, continuing to build as we normally would and serving as a workaround for the issue.
This can be helpful when the same macro names, class names, global variables and others are redefined by different
components.