DUECA/DUSIME
Loading...
Searching...
No Matches
Hints on CMake use

Introduction

As of early 2021, a modified project structure and build system for DUECA was completed. The old scripts (dueca-project) used CVS (Concurrent Version System) as a back-end for version control, and an elaborate structure with Makefiles and scripts to compile and link the code of DUECA projects.

There were a number of reasons to change this, first and foremost is the aging status of CVS, and while not becoming unavailable, fewer and fewer people know how to use it.

Another issue is the work needed when using the Makefile system. Most DUECA programmers will have encountered a point where a missing library or header is preventing a successful DUECA build. When borrowing a module from another project, often the main project Makefile needs to be adjusted to link associated libraries.

The dueca-gproject script

A new script was created, dueca-gproject, that differs from the old script in a number of ways:

  • The script uses git as back-end for the version control. In contrast to dueca-project, pure version control commands are not included in the script, as those can be performed by running git directly. Only specialised actions, like adding the files generated by the script and the particular way of checking out a git repository is performed by the script.
  • The build system relies on the build tool CMake to produce generated code from the .dco files and compile and build the application.
  • The project configuration is slightly improved. The old comm-objects.lst files are still there, but other configuration is moved to a hidden folder labeled .config. This folder contains:
    • A tiny text file called machine, which defines which configuration is checked out (the "machine class"). A machine class can for example be "ig", for Image Generator, computers configured with typically scene graph software to generate the outside image.
    • An xml file called machinemapping.xml, which defines which dueca node (computer in a lab or equivalent), runs which kind of machine.
    • A folder class, which lists one or more sub-folders, each named after a machine class. Each of these sub-folders contains two files. The first is modules.xml, which defines which modules (DUECA components) are used in a specific machine configuration. The second is config.cmake, an additional configuration file for the cmake build system. This file is normally unchanged, only for specific machine classes (notably computers with hardware attached to them, and specific libraries to access that hardware), it is used to list which hardware Input/Output libraries are to be linked with the project.
  • There is also a folder build in the main project directory. Initially it is empty, it is used to store all build products (generated files, compiled code, and ultimately the DUECA executable). KEEP THIS FOLDER CLEAN. Don't put your files in here, everything here will be automatically generated from your source files elsewhere.

Leveraging the power of CMake

For finding different libraries or support programs, CMake provides a large range of functions that you can call. These functions set variables in your workspace, you can then use these variables to indicate where include files are located and which libraries should be linked. A nice feature of CMake's detection is that it can also warn you when libraries or headers are not found.

A commonly used function in CMake is pkg_check_modules, which you can run with all libraries and programs that have so-called pkg-config files. You find these pkg-config files in folders like

  • /usr/lib/pkg-config
  • /usr/lib64/pkg-config
  • /usr/lib/x86_64-linux-gnu/pkg-config

Another common function is find_package, which can be used for libraries and packages that have specific cmake configuration files written for them. Check the folder /usr/share/cmake/Modules to check with Find files are provided by the cmake installation.

If you have a library or header files for which these facilities are not provided, you can use cmake's basic functions find_path and find_library to search for header installation and libraries.

The final section of this chapter shows some common detection patterns.

Overview of CMake files

A DUECA project uses three types of CMake files; a main file for the whole project, a specific included file for a machine class, and two variants of files for the module directories and the comm-objects folder.

Main CMakeLists.txt file.

You will find the main CMakeLists.txt file in the main project directory (e.g., MyProject/MyProject). This file defines which scripting language is used, and defines the DUECA components that are used in the project, on all different machines. Generally no modifications are needed for this file.

Machine class config.cmake file.

A DUECA project can be built for different machine classes. A machine class defines which modules are to be included in the compilation, and optionally which specific libraries are needed for that machine class.

The config.cmake file describes adaptations specific for a machine class. Modifications are often not needed. Only for specific machines, and in our example these are often the control loading computers in the simulators, that interact with a slew of hardware to a library specific to the computer, need some tweaks. In that case, since the modifications are specific to a lab, and only on one or a few computers of that lab, it is appropriate to adjust the config.cmake file that you find in the .config/class/<machineclass> folder. Generally, the location of the header files for the IO library, and the IO library files, are indicated there.

Per-module CMakeLists.txt file.

Each module also has a CMakeLists.txt file, in the module folder. When your module uses specific software external to your project, such as a scene graph library, math software, or the like, this can be indicated in that CMakeLists.txt. Also if you have for example two or more modules that use, e.g., an xml library, please use the per-module CMakeLists.txt. CMake itself will ensure that the detection of software is cached, and only done once, and the libraries you indicate are also only linked once. Placing the cmake calls here, instead of in the main CMakeLists.txt file, ensures that the modules can also be borrowed and re-used in other projects individually.

The comm-objects folders also have a CMakeLists.txt file that mostly resembles the one in the modules. The only difference is that there is a target added for the generated code. You can adapt this file in the same way as for the module CMakeLists.txt. Note that, unless you define super-smart dueca communication objects (that maybe use matrix libraries or the like), adapting this file is seldom needed.

Common approach

Running CMake

To use cmake, you call the program cmake from any directory, and the only required argument is the directory where you main CMakeLists.txt file is located. For DUECA projects, we use the folder build, and after entering that folder (cd build), call cmake with:

cmake ..

It is also possible to specify variables for the build system in the cmake call, but for DUECA projects this is normally not used. Any specific configuration or adjustment can be done per machine class, in the machine class config.cmake file.

If successful, this has now filled the build directory with files needed for building the project. The default build system uses Makefiles, so from this build directory, call

make

to start compilation. In some cases, if you have adapted your code or configuration, this might automatically re-invoke cmake to update the build system.

Adapting the CMakeLists.txt files

If you know which library you need for compiling your module, try to find a CMake call to find the headers and library files. As a general rule, CMake-specific information, which is provided through the find_package call with the proper arguments, is to be preferred. Some very general libraries have files provided in CMake's installation, in the folder /usr/share/cmake/Modules.

As an example:

(set BLA_VENDOR OpenBlas)
find_package(BLAS)

In general, the files are well documented, often search options (as here, for a specific BLAS variant, after setting a specific variable), are possible.

After such an invocation, the find_package script will leave result variables in the cmake program. By convention, there is always a variable to indicated success (in this case BLAS_FOUND, and variables that give the list of libraries, include paths (which blas does not do), or other components.

In this case, you would add ${BLAS_LIBRARIES} to the LIBRARIES specified in the dueca_add_module call.

Another option, if the default CMake search is not possible, is to use a different system, pkg-config, to get the information on a library.

pkg_check_modules(TINYXML tinyxml REQUIRED)

This sets variables TINYXML_FOUND, TINYXML_LIBRARIES and TINYXML_INCLUDE_DIRS. Note that here the variable prefix (here TINYXML) was specified by you. The tinyxml corresponds to the name of the pkg-config file.

Adding the keyword REQUIRED makes CMake configuration stop when the library is not found. Again, use the produced variables (TINYXML_INCLUDE_DIRS and TINYXML_LIBRARIES) to indicate which header locations to search and which libraries to include, like in the following example:

# add current module as target, optionally add required dueca components
dueca_add_module(
  SOURCES ${SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/comm-objects.h

  # specify include directories, add for used libraries as needed
  INCLUDEDIRS ${DCO_INCLUDE_DIRS} ${TINYXML_INCLUDE_DIRS}

  # optionally add more DUECA components (e.g., a graphics toolkit?)
  # DUECA_COMPONENTS

  # if you directly use code from other modules here, specify these
  # in the form of project/module. Code generation order will be correct,
  # and the source location of these modules is added to the include path
  USEMODULES

  # optionally specify libraries used for this module
  LIBRARIES ${TINYXML_LIBRARIES} ${BLAS_LIBRARIES}

  # give compiler options
  COMPILEOPTIONS ${CUSTOM_OPTIONS}
)

In case none of that is available, there are more primitive ways to check the location of a library or header, with find_path and find_library.

Folders used by the CMake build

You have seen from the snippets above, that CMake variables can be accessed by enclosing them in curly brackets and a Dollar sign: ${MY_VARIABLE}. When using a find_package call, inspect its documentation (or search for it on Google or the like), to see which variables it sets.

The build system uses the build folder for generated code and build results, and the project folder (e.g., MyProject/MyProject) as basis for the source. In the CMake scripts, these places are defined by the following variables:

${CMAKE_SOURCE_DIR}
${CMAKE_BINARY_DIR}

In general, it is best to keep modules independent from each other, and have them only connect over the DCO files they communicate. However, quite often it is useful to put helper code in modules, that is used directly by other modules.

Thus, if you did borrow a module called borrowed-module, from a project called OtherProject, and you need to include a header from this folder, you should add the following to the USEMODULES of the module where you need that header:

dueca_add_module(
  SOURCES ${SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/comm-objects.h

  # specify include directories, add for used libraries as needed
  INCLUDEDIRS ${DCO_INCLUDE_DIRS}

  # optionally add more DUECA components (e.g., a graphics toolkit?)
  # DUECA_COMPONENTS

  # if you directly use code from other modules here, specify these
  # in the form of project/module. Code generation order will be correct,
  # and the source location of these modules is added to the include path
  USEMODULES
  OtherProject/borrowed-module

  # optionally specify libraries used for this module
  LIBRARIES

  # give compiler options
  COMPILEOPTIONS ${CUSTOM_OPTIONS}
)

It would also be possible to add the path to the module source to the INCLUDEDIRS argument, like this:

  # specify include directories, add for used libraries as needed
  INCLUDEDIRS ${DCO_INCLUDE_DIRS}
  ${CMAKE_SOURCE_DIR}/../OtherProject/borrowed-module

(As a side note, ../ walks up one folder, then OtherProject/ enters the OtherProject folder, etc.). However this approach may fail, because most modules rely on generated code as well. The file comm-objects.lst will be converted to a file comm-objects.h, that includes all communication (DCO) objects used by a specific module. If you don't tell CMake, this file generation might take place too late. Adding the used module to USEMODULES, ensures both that the code generation in the used module takes place first, and that the correct paths are set.

The two variables:

${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}

indicate the location of the source and generated code of the current module or comm-objects folder. As you can see in the example dueca_add_module call above, the file comm-objects.h, which is generated from a comm-objects.lst file in your source folder, ends up in the ${CMAKE_CURRENT_BINARY_DIR}, and since it is part of the module, needs to be added to the sources. Note that CMake sources include both header files (.h, .hxx) and source files (.c, .cxx, even Fortran; .f, etc). CMake is smart enough to determine which sources to compile. Adding also the headers enables CMake to figure out when to recompile after edits in header files.

In general, avoid using absolute – and computer dependent – paths in your CMake configuration; use CMake's detection mechanisms to find out where headers and libraries are located, and for finding files in borrowed DUECA modules use relative paths starting from the CMAKE[_CURRENT]_SOURCE_DIR and CMAKE[_CURRENT]_BINARY_DIR.

The DCO code generation

The CMakeLists.txt file in the comm-objects folders also has instructions for the code generation. Occasionally these need to be tweaked; when using a fixed-length array, where the length of the array is determined from a C++ enum variable or a define variable, a small program needs to be generated to get this array length. This program might need to find included headers, and you can add an INCLUDEDIRS variable, much like for the dueca_add_module call, to indicate the location of these header files. An example:

# specify codegen target to convert the DCO sources to C++
# sets DCO_OUTPUTS to a list of all generated source
duecacodegen_target(OUTPUT DCO DCOSOURCE ${ACTIVEDCO}
  INCLUDEDIRS ${CMAKE_CURRENT_SOURCE_DIR})

Recipes for CMake use

In the following, some common recipes for finding software are discussed.

Gtk+extra, versions 2.0 and 3.0

These two versions are mostly compatible. The following snippet finds the latest one:

pkg_check_modules(GTKEXTRA gtkextra-3.0)
if(NOT GTKEXTRA_FOUND)
  pkg_check_modules(GTKEXTRA gtkextra-2.0 REQUIRED)
endif()

Results:

Variable Description
GTKEXTRA_FOUND Success indicator
GTKEXTRA_LIBRARIES Libraries
GTKEXTRA_INCLUDE_DIRS location of the header

Platform-specific configuration

The control loading application needs to link with platform specific IO libraries, depending on the computer where the control loading is run, number of axes and capabilities of the control loaders differ. This is added to the machine class cmake.config file for the respective platform. Here is an example for the HMI laboratory control loading, it uses basic find_path and find_library calls:

# find distinctive hmi-io headers
find_path(HMIIO_INCLUDE_DIR 
          NAMES HardwareInterface.hxx hmiio_calib_volts.h REQUIRED 
          PATHS $ENV{HOME}/dapps/hmi-io)
list(APPEND HMIIO_INCLUDE_DIRS 
     ${HMIIO_INCLUDE_DIR} ${HMIIO_INCLUDE_DIR}/build)

# check that the library is there
find_library(HMIIO_LIBRARY NAMES hmi-io REQUIRED 
             PATHS $ENV{HOME}/dapps/hmi-io/build)

# ethercat
pkg_check_modules(ETHERCAT libethercat REQUIRED)

# pass the result to the project
set(PROJECT_LIBRARIES ${HMIIO_LIBRARY} ${ETHERCAT_LIBRARIES})
set(PROJECT_INCLUDE_DIRS ${HMIIO_INCLUDE_DIRS} ${ETHERCAT_INCLUDE_DIRS})

Results:

variable description
HMIIO_LIBRARY Library
HMIIO_INCLUDE_DIRS location of the headers

The code for the SIMONA Research Simulator is similar, enter it in .config/class/srs-io.cmake:

# find distinctive srs-ecat-io headers
find_path(SRSIO_INCLUDE_DIR 
          NAMES HardwareInterface.hxx build/srs-ecat-io-config.h REQUIRED 
          PATHS $ENV{HOME}/apps/srs-ecat-io)
list(APPEND SRSIO_INCLUDE_DIRS 
     ${SRSIO_INCLUDE_DIR} ${SRSIO_INCLUDE_DIR}/build)

# check that the library is there
find_library(SRSIO_LIBRARY NAMES srs-ecat-io REQUIRED 
             PATHS $ENV{HOME}/apps/srs-ecat-io/build)

# pass the result to the project
set(PROJECT_LIBRARIES ${SRSIO_LIBRARY})
set(PROJECT_INCLUDE_DIRS ${SRSIO_INCLUDE_DIRS})

Results:

variable description
SRSIO_LIBRARY Library
SRSIO_INCLUDE_DIRS location of the headers

Tinyxml header

Tinyxml is one of the easy and commonly used XML libraries. Version 1 and 2 are available, and unfortunately these are not compatible, so a switch to version 2 requires adaptation to your code.

# TINYXML 1
pkg_check_modules(TINYXML tinyxml REQUIRED)

# TINYXML 2
find_package(tinyxml2)

Results:

variable description
TINYXML_LIBRARIES Library
TINYXML_INCLUDE_DIRS location of the headers
TINYXML2_LIBRARIES Library
TINYXML2_INCLUDE_DIRS location of the headers

Pugixml

Pugixml is another commonly used XML library. It is more modern C++ than TinyXML.

pgk_check_modules(PUGIXML pugixml REQUIRED)

Results:

variable description
PUGIXML_LIBRARIES Library
PUGIXML_INCLUDE_DIRS location of the headers

OpenSceneGraph

OpenSceneGraph can show 3D worlds on out of the window displays.

find_package(OpenSceneGraph REQUIRED COMPONENTS osgDB osgUtil osgViewer)

Results:

variable description
OPENSCENEGRAPH_LIBRARIES Library
OPENSCENEGRAPH_VERSION Version
OPENSCENEGRAPH_INCLUDE_DIRS location of the headers

Simulink Coder projects

Simulink coder projects have a small file defines.txt, that describes the required defines for compiling the code. The following snippet runs through this file, and adds the defines to CUSTOM_OPTIONS:

# RTW defines
file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/defines.txt DEFINESTXT)
foreach(L IN LISTS DEFINESTXT)
  if(NOT L STREQUAL "RT")
    list(APPEND CUSTOM_OPTIONS "-D${L}")
  endif()
endforeach()

A small note: This skips the RT option (dueca-config will add that one for you), since defining -DRT means defining the common letter combination RT to nothing, and that breaks quite a lot of code in deep and troubling ways. Such a define simply replaces occurrences of RT in your C or C++ code. dueca-config keeps that sane by defining RT=RT. Add the appropriate rtwvxx_xx to the DUECA_COMPONENTS argument in your CMakeLists.txt file of the module with the rtw code.

HMILibToolkit files

The hmi toolkit can support the development of instrument displays. Interface header and source files are generated from .if files, to add this generation to your CMakeLists.txt file, use the following snippets:

# get support code
include(HMILibAddIf)

# find all the if files. Note that there are often several
# in the 'components' folder
file(GLOB HMICOMP "components/*.if")
set(HMILIBFILES .... ${HMICOMP})

# generate the conversions
hmilib_add_if(HMILIB_SOURCES HMILIBSRC HMILIB_IF ${HMILIBFILES})

Now the variable HMILIBSRC contains a list of the generated sources. Add that to the dueca_add_module call to ensure that these are being built.

dueca_add_module(
  SOURCES ${SOURCES} ${CMAKE_CURRENT_BINARY_DIR}/comm-objects.h
  ${HMILIBSRC}

  # specify include directories, add for used libraries as needed
  INCLUDEDIRS ${DCO_INCLUDE_DIRS}

  # to get to the hxx files that are under components, both
  # generated and in source
  ${CMAKE_CURRENT_BINARY_DIR}/components
  ${CMAKE_CURRENT_SOURCE_DIR}/components
...