|
DUECA/DUSIME
|
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.
A new script was created, dueca-gproject, that differs from the old script in a number of ways:
.config. This folder contains: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.machinemapping.xml, which defines which dueca node (computer in a lab or equivalent), runs which kind of machine.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.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.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-configAnother 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.
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.
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.
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.
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.
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.
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.
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 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})
In the following, some common recipes for finding software are discussed.
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 |
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 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 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 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 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.
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
...