Using CMake in Simics

See Simics CMake Reference Manual for details on the Simics CMake package.

1 Introduction to CMake

1.1 Why CMake?

There are many reasons for using CMake instead of GNU Make. Some are opinionated while others are not. The major upsides comparing these two are:

1.2 What is CMake

CMake is a build system generator. It defines targets (modules, libraries, executables, custom) and their dependencies. The dependency scope can be private, transitive (interface) or both (public). This is all defined in CMakeLists.txt files starting from the root and adding more files as they are consumed by CMake.

The build system is generated into a build tree by invoking CMake. The path to the source tree root, the path to the build tree and the type of build system to generate (Ninja, GNU Make, etc) are the only required inputs. Additional configuration parameters such as the build type (Debug or Release) and where to locate the compiler etc can be provided from CLI or through some UI.

Once the build system has been generated, it can be run through the build system runner (Ninja, GNU Make, etc) directly from the build tree "just like normal" or through the CMake do-it-all binary (i.e. cmake --build <path to build tree> --target <target>).

1.3 The key to understand CMake - Targets

The best way to understand how CMake works is to think of everything as targets. A target is what you build, for example an executable or a library. It probably depends on a few libraries and those are also targets. It might depend on a header-only library and that is also a target. All targets are linked (not necessarily by a linker, but conceptually) using target_link_libraries. The scope, properties and features of each target defines how it will interact with other targets.

2 Minimum requirements

Currently the minimum required version of CMake is 3.22.

The requirement comes from our use of ENVIRONMENT_MODIFICATION in simics_add_test() function.

The latest is the greatest, as later versions not only contains new features but also bugfixes and improved performance. Thus the recommended version is the latest version.

Installing CMake locally should be easy but if access to the host's package management system is not granted or it does not provide a recent enough version; installing CMake (and Ninja) in your local folder or home directory can be done swiftly using pip.

Running with at least 3.24 is preferred as it adds the --fresh command line option that allows you to re-run the configuration without any of the cached variables.

3 Current limitations

These are the known limitations:

4 Recommendations

5 Working with CMake: sources of reference

These sources of reference will give you everything you need:

6 Creating a Simics project with CMake support

$ <path to Simics Base>/bin/project-setup . --with-cmake

If a CMakeLists.txt file already exists, run with the --force flag.

7 Building modules

7.1 General usage

CMake is not a build system in itself, it is a generator of build systems.

Thus the traditional GNU Make driven build process is, by design, split in two parts when working with CMake:

It's important to make this distinction. It allows CMake to split the collection of static properties from dynamic properties collected during each build. This allows the build to run much faster.

This is also the reason why CMake frowns upon globbing, and so this should be avoided if possible.

Simics expects to locate Simics modules in the "host lib" folder, e.g. <project>/linux64/lib, so this is where Simics CMake targets emits them. The intermediate files remains in the build directory configured by the user. Unlike the old GNU Make driven flow a CMake driven flow can have multiple build directories which could be useful to separate debug and release builds for example.

7.2 Build modules using the standard CMake program

NOTE: This is the recommended approach

The simplest option (after getting used to it) is probably to use the cmake program from CLI. On Linux with bash-completions enabled you can tab-complete the options (but not the targets) and it contains a lot more than just the --build option making it a very useful tool to master.

This is how to bootstrap (i.e. generate a build system) and build everything:

$ cmake -S . -B bt -G Ninja -DCMAKE_BUILD_TYPE=Release
$ cmake --build bt

Where:

See cmake --build bt --help for more details. Here is a list of useful options:

There is also the ccmake program, which is an ncurses frontend to configuring the build directory:

$ ccmake bt

7.3 Build modules using tiny GNU Make wrapper

NOTE: This is not the recommended approach, only listed for reference

During the initial transition from GNU Make to CMake, before getting acustomed to the cmake tool, it might feel more convenient to continue with a make-driven flow:

$ make
$ make <target>

NOTE: this depends on the following files and local changes:

-include cmake-wrapper.mk

The GNU Make wrapping, provided by cmake-wrapper.mk, handles the following:

LIMITATIONS:

7.4 Build modules using explicit invocation of generated build system

Not covered by this documentation. It's recommended to run through the cmake --build indirection.

7.5 Build modules using your favourite IDE that supports CMake

Not covered by this documentation.

8 Testing modules

The Simics CMake package adds support for running tests via CTest and enables this support by default.

To run all available tests:

$ ctest --test-dir bt

Useful parameters to control how the tests are run:

Simics tests are added to CTest via the simics_add_test() function. See Simics CMake Reference Manual for details.

9 More details on how it works

CMake expects the root of the source directory (passed to cmake) to contain a top-level CMakeLists.txt file that defines the CMake project. This file sets up compiler settings (but not the compiler!) and defines what libraries and executables to build. Typically the configuration for these libraries and executables are located in separate CMakeLists.txt files added to the project by calls to add_subdirectory().

The Simics CMake package provides the simics_add_module() function, and some other helpful functions described below, to help the user define what to build in the project. See Simics CMake Reference Manual for details and basic usage of all functions.

In order to use the Simics CMake package it must be added to the project configuration:

    find_package(Simics REQUIRED)

This requires that CMAKE_PREFIX_PATH has been set to ${SIMICS_BASE}/cmake where SIMICS_BASE is the absolute path to the Simics Base installation. The Simics project, when created with the --with-cmake flag, comes with a default CMakeLists.txt file that provides this path explicitly:

    find_package(Simics REQUIRED CONFIG NO_DEFAULT_PATH PATHS ${SIMICS_BASE}/cmake)

The default top-level CMakeLists.txt file generated by project-setup invokes the simics_find_and_add_modules() function which:

LIMITATIONS:

NOTE: CMake will take care of providing the paths of the added modules to the compiler so it is no longer a Simics project requirement that modules must be copied into the project in order to build them.

As a user you are not required to use the top-level CMakeLists.txt provided by project-setup. Nor are you required to call simics_find_and_add_modules, as the basic functionality is provided by the Simics CMake package. In fact, you are not even required to create a Simics project. See simics_project_setup() in the Reference Manual for a detailed description of the supported modes. Using CMake provides a lot more flexibility over the old GNU Make driven Simics module build system.

To build all Simics modules registered with CMake, build the simics-modules target. To list all modules, build the list-simics-modules target.

10 Converting an existing GNU Makefile to CMakeLists.txt

Conversion follows these three steps.

  1. Run the gmake-to-cmake converter to get a good starting point. For example:

    $ ./bin/gmake-to-cmake modules/AT24Cxxx
    
  2. Make note of any warnings or errors shown during conversion, for example

    WARNING: MODULE_CFLAGS used, please review:
    
  3. Make adjustments as necessary by comparing Makefile and CMakeLists.txt side-by-side

Please note that the converter is not meant to handle all types of input, and it only detects and reports a small set of problems. It should be used as a starting point only, as writing a tool that understands GNU Make is out-of-scope for the Simics CMake project.

For trivial modules, such as the sample-device-* modules, the converter works and can be trusted. But for more complicated modules that use GNU Make logic, conditional code, generates files, expands and filters variables to construct new lists, etc etc; the user must conduct a manual review.

For shared common code and other folders that does not define a Simics module it's probably easier to start from scratch with an empty CMakeLists.txt or use some existing common code as template.

The following sub-sections labeled A..F provides details and examples of how to solve some of the common problems with constructing a CMakeLists.txt file for Simics.

A) Makefile is using variable references

Most of the time it's better to expand these indirections and use explicit names for classes and source files. Where indirections are warranted CMake does support variables via set(...) function. CMake has an extensive library of utility functions that operates on variables and lists to handle the most common problems.

B) Makefile is using wildcard to locate files

Most of the time it's better to explicitly list all the files so CMake can track their dependencies properly. CMake does have support for path pattern matching via file(GLOB ...) function but these pattern matches are only run during configuration phase and not between consecutive builds; which means you have to explicitly reconfigure the project if new modules are added to the CMake project. To mitigate this, CMake's file(GLOB ...) has a CONFIGURE_DEPENDS option that causes the pattern match to be re-evaluated on every build. There is a cost involved of course, so use with caution and avoid if possible.

C) Makefile is referencing files from common code via EXTRA_MODULE_VPATH

Please note that this section is about common code. See (D) below for referencing files from other modules.

There are two distinct ways common code is used by modules, and they need different solutions.

C1) Makefile is not passing custom defines to the common code

In this case, it's always better to let the other module build a static library and add a dependency on that library target. This is done using add_library(...) and target_link_libraries(...):

In module A (the user):

target_link_libraries(A PRIVATE B)

In module B (the provider):

add_library(B STATIC 1.c 2.c 3.c ...)

The conversion of module A is handled by the converter, but the conversion of module B has to be done manually. In order to build module B it's likely that include paths must be added explicitly, and this is done by target_include_directories(...). Paths to Simics standard includes are otherwise added to module A via the simics_add_module(...) function, but B cannot depend on Simics::Simics as it's a STATIC. Instead we add a dependency on Simics::includes.

In module B:

target_include_directories(B
  INCLUDE .)
target_link_libraries(B
  PRIVATE Simics::includes)

In the snippet above keywords PRIVATE and INCLUDE are used to control the scope and transitivity of these configurations. PRIVATE means it only applies to the current target. INCLUDE means is only applies to targets that depend on the current target. Configuration that should apply to both the current target and targets that depend on the current target, must use the PUBLIC keyword. The use of . in target_include_directories is expanded within B to the current source path of B, but added to the include paths of A.

C2) Makefile is passing custom defines to the common code, or is just referencing the headers

In this case, unlike (C1), the referenced source files (if any) must be built by module A to honor the module specific defines. To achieve this in CMake we define an INTERFACE library instead of a STATIC library. INTERFACE libraries do not produce any output; they are used to pass values and can be used as target dependencies:

In module A:

target_link_libraries(A PRIVATE B)
add_compile_definitions(DEVICE_NAME=A)

In module B:

add_library(B INTERFACE)
target_sources(B INTERFACE 1.c 2.c 3.c ...)
target_include_directories(B INTERFACE .)

Here INTERFACE means module A (the user) and the sources listed by module B (the provider) are added to module A. The obj files produced are put into module A's build directory and will not be re-used by any other module that also depends on module B. Please note that since the files are built by A and A gets Simics include paths added by simics_add_module there is no need to explicitly depend on Simics::includes here; unlike in (C1).

An INTERFACE type library does not have to provide any sources, it can just provide include directories. This is useful in (D2) below. Specifically for DML modules common code is typically shared this way:

add_library(cmn-common INTERFACE)
target_include_directories(cmn-common INTERFACE .)
add_library(cmn-common::imports ALIAS cmn-common)

D) Makefile is referencing files from other modules via EXTRA_MODULE_VPATH

There are two type of files: source files and header files.

D1) Source files

To share source files between modules a STATIC type library as described in (C1), or an INTERFACE type library as described in (C2), must be created and given a unique name. By convention the NAME given to simics_add_module is the module name and cannot be re-used:

In module A

target_link_libraries(A PRIVATE B::shared)

In module B

add_library(B-shared STATIC event-queue.c)
target_link_libraries(B-shared PRIVATE Simics::includes)
add_library(B::shared ALIAS B-shared)

or

add_library(B-shared INTERFACE)
target_include_directories(B-shared INTERFACE .)
target_sources(B-shared INTERFACE foo.c)
add_library(B::shared ALIAS B-shared)

'B-shared' can be any name not already present in the CMake configuration. It is recommended to provide an alias to clearly indicate that 'shared' is a target in the B module.

D2) Header files

The simics_add_module function automatically provides an INTERFACE type library, as described in C2, in addition to the MODULE type library; adding the current module directory as target include directory. The following three aliases can be used for this INTERFACE type library: <MODULE_NAME>::includes, <MODULE_NAME>::headers and <MODULE_NAME>::imports. They all work the same and differ only by name, to provide some syntactic sugar matching the language used by the Simics module.

In module A:

target_link_libraries(A PRIVATE B::includes)

In module B: no changes needed as the INTERFACE type library is auto-generated.

It might be tempting to use the 'MODULE_NAME' directly in target_include_directories, but this does not work. Simics modules must be fully isolated entities without runtime dependencies and thus are created with MODULE library type. This prevents CMake from linking a Simics module to anything else.

E) Makefile is generating files based on other files

This is a common pattern in Simics, for example module_load.py which is generated from common code fetched from some other module. To solve this problem, use the CMake standard add_custom_command with built-in cat functionality:

add_custom_command(
  OUTPUT module_load.py
  COMMAND ${CMAKE_COMMAND} -E cat a.py b.py > ${CMAKE_CURRENT_BINARY_DIR}/module_load.py
  DEPENDS a.py b.py
)

Also make sure that generated files are part of SOURCES. Generated files should be output in the current binary directory, simics_add_module searches first the current source directory and then the current binary directory for Python and DML files.

See sample-risc/CMakeLists.txt for an example of this.

F) Copy python files from other modules

Sometimes a module needs to re-use python files from other modules as part of its own simmod structure. This can be done using standard CMake functions, but since it's fairly common a convenience method has been provided:

   simics_copy_python_files(ICH10 FROM ICH FILES ich_commands.py ich_updaters.py)

See simics_copy_python_files() documentation in the Reference Manual for more details on usage.

11 General notes / Tips and tricks

The CMake+Ninja combo is the best/fastest for CLI based development. More powerful IDEs might leverage CMake differently. See CMake documentation for details. Here are a couple of tips to get started.

11.1 Built-in help on CLI

cmake --help contains everything you need. Especially --help-command to learn more about each command. The bash shell supports tab-completion out-of-the-box so it's easy to navigate. Of course, there is also the cmake.org website.

11.2 Use build-in CMake commands to stay portable

The cmake -E utility provides portable ways to do many file operations such as cat and should be used over if-conditional code:

   COMMAND ${CMAKE_COMMAND} -E cat a.py b.py > ${CMAKE_CURRENT_BINARY_DIR}/module_load.py

See cmake -E --help or the online documentation for more details.

11.3 Keep things as local and "targeted" as possible

For example, and as the documentation also states, use the target_include_directories instead of include_directories etc. The target_-prefixed versions of their counterpart require one of the INTERFACE, PUBLIC or PRIVATE keywords to define the scope of the command:

Use target properties and avoid globals.

11.4 Use the CMake API as intended

In the GNU Make driven Simics build system all flags added to MODULE_CFLAGS was passed to the compilation step and all flags added to MODULE_LDFLAGS was passed to the linking step. The CMake API provides functions at a finer granularity for expressing these things:

The converter does not try to be clever and solve this problem; it just warns about it. For clarity it is important that flags are passed using a combination of these function calls. Please note that the user must also classify the scope of the flags, i.e. PRIVATE, INTERFACE or PUBLIC.

11.5 Use log-level to differentiate messages

CMake has defined a set of log-levels that should be used to differentiate messages. The most important ones are:

The log-level to use can quickly be changed by passing --log-level to cmake.

See cmake.org:message() for more details and more log levels.

11.6 Tracing support

Correct use of message log-levels can improve debuggability, but should that not be enough there is always the sledge hammer!

$ cmake --trace ...

This emits a lot of details to stdout about what cmake is doing when processing the CMakeLists.txt files. To limit the output to only a few files of interest, add the --trace-source option.

See cmake.org:--trace for more details.

11.7 Printf-debugging

CMake has built-in utility functions for printf-debugging:

See cmake.org:CMakePrintHelpers for details.

11.8 Don't forget to build static libraries

In general, and hence by default, you want to build shared libraries as they are easier to share among your build targets. But in Simics, this breaks module isolation and must be avoided. So remember to pass STATIC when building helper libraries, i.e.: add_library(<target> STATIC ... )

11.9 Must build with -fPIC

By default the Simics CMake configuration will set CMAKE_POSITION_INDEPENDENT_CODE to ON which enables all STATIC library builds to pass the -fPIC flag to the compiler. If this is not the case, it can be selected with target_compile_options(<target> PRIVATE -fPIC)

11.10 To share headers between modules, create an INTERFACE library

See D2 (for modules) or C2 (for common code) for more details.

Note that shared/imported .dml files counts as headers in this case.

11.11 To share files, create a STATIC library

See D1 (for modules) or C1 (for common code) for more details.

In the GNU Make driven build system, code sharing was done by adding the other.c file to the SRC_FILES variable in the Makefile of the module where it was going to be used. Causing the same files to be built over and over multiple times. Though this is still possible to do by explicitly providing the absolute path to the file, it is not the recommended approach in CMake.

E.g. instead of doing this:

   target_sources(versatile-devices
      PRIVATE ${SIMICS_PROJECT_DIR}/src/extensions/keycodes-common/keycodes.c)

you should be doing this:

   target_link_libraries(versatile-devices
      PRIVATE keycodes-common ...)

where keycodes-common defines a STATIC library like this:

   add_library(keycodes-common STATIC keycodes.c)
   target_include_directories(keycodes-common PUBLIC . ...)

Please note that special flags and defines set by the target where this static library is used, do not propagate into the build of the static library. Such flags must also be set on the static library or the source files should be compiled as part of the "user" target as described by (C2). For example, to build for SIMICS_API=6 one must pass target_compile_definitions(<target> SIMICS_6_API).

11.12 Set RPATH

The Simics CMake package provides functions and targets to build Simics modules. The Simics modules are meant to be dynamically loaded by Simics and as such can rely on Simics to have loaded all the libraries a module depends on, e.g. vtutils and python. External dependencies should be avoided, to allow the module to be relocatable to other hosts.

Other binaries built by the same project, such as utilities and unit tests, might require RPATH being set though and this can be done on-demand by each target by setting the BUILD_RPATH property:

   set_target_properties(generate-dml-from-xml
        PROPERTIES BUILD_RPATH $ORIGIN/libs:${SIMICS_LIB}:${SIMICS_SYS_LIB})

Passing explicit linker options also works, but should be avoided:

   target_link_options(${MODULE_NAME} PRIVATE -Wl,-rpath,${SIMICS_SYS_LIB})

Projects that build mostly other things can setup RPATH globally in the top-level CMakeLists.txt using the CMAKE_BUILD_RPATH cache variable. See official CMake documentation for more details.

11.13 Setting properties per source file

Sometimes a set of flags only apply to a subset, or just one, of the files that make up a target. Setting the flags on the target might then be suboptimal as it would affect everything built within that target. Here one could use set_source_files_properties to alter properties per source file.

For example, if a compilation unit does not compile with -O3 the optimization can be reduced per unit.

Another example is, if _FORTIFY_SOURCE=2 has been enabled then that requires __OPTIMIZE__ > 0 ; so if the current optimization level is to be reduced to zero one must also undefine _FORTIFY_SOURCE:

    set_source_files_properties(zuc.c
        PROPERTIES COMPILE_OPTIONS "-O0;-U_FORTIFY_SOURCE")

12 Coverage with GCOV/LCOV

To configure a build tree for generating GCOV coverage the following two conditions must be met:

  1. Add the following line to top-level CMakeLists.txt file: include(${SIMICS_BASE_DIR}/cmake/coverage.cmake)
  2. Configure a build tree with USE_COVERAGE=1 and provide LCOV, GCOV and GENHTML if binaries are not already in PATH. See below for details.

The coverage.cmake adds the following targets:

12.1 How it works

Enabling USE_COVERAGE=1 will enable the following flags for coverage:

    add_compile_options(--coverage -g -Og)
    add_link_options(--coverage -Wl,--dynamic-list-data)

Then a normal build will produce the GCOV instrumented binaries and a normal run (or sequence of runs) of some tests/work-loads will generate the aggregated coverage data.

Once coverage data has been generated, a coverage report can be generated via the generate-coverage-report utility target. The HTML report is generated in <build-tree>/coverage/index.html

See example below for details and step-by-step on all of this.

12.2 Configuration

The targets are enabled only if LCOV binary is found in PATH or if the path to LCOV is provided in the LCOV cache variable.

Assumptions and cache variables used by coverage.cmake:

12.3 Example

  1. Configuration:
    $ cmake -S . -B btc -G Ninja -DCMAKE_BUILD_TYPE=Debug -DUSE_COVERAGE=1 -DLCOV=/usr/itm/lcov/1.16/bin/lcov
  1. Build the modules
    $ cmake --build btc
  1. Initialize a baseline. This is important, and documented in the LCOV manual, so that "to ensure that the percentage of total lines covered is correct even when not all source code files were loaded during the test.":
    $ cmake --build btc --target init-coverage
  1. Run some tests to generate coverage data.
    $ ctest --test-dir btc -j40
  1. Generate the report
    $ cmake --build btc --target generate-coverage-report
  1. Optionally (for internal use): generate coverage including Simics libraries:
    $ cmake --build btc --target generate-coverage-report-internal
  1. Inspect the report in btc/coverage/index.html

  2. To clear/reset the coverage counters between reports:

    $ cmake --build btc --target reset-coverage

13 Sanitization with ASAN and UBSAN

Sanitizers are a great way to find additional bugs during run-time. A typical flow would be to compile targets used in tests with ASAN and UBSAN, to get tests with a higher bug coverage. More information about what type of bugs ASAN and UBSAN can reveal can be found at https://github.com/google/sanitizers. Keep in mind that sanitizers are not supported on Windows.

To add ASAN/UBSAN conditional compilation for a Simics module, you need to call the simics_add_sanitizers function with your module name as the parameter. Example:

simics_add_module(my-device
  CLASSES  my_device
  SOURCES my-device.dml
  SIMICS_API 6
)
simics_add_sanitizers(my-device)

This will mark the my-device module as a module that should be compiled with sanitizers when sanitizers are enabled. To enable compilation with sanitizers, you should set the following CMake variables accordingly. Note that you can enable each option individually or all at once.

VariableEnables
USE_UBSANUndefinedBehaviorSanitizer
USE_ASANAddressSanitizer
ASAN_STACK_USE_AFTER_RETURNStack-use-after-return (enables USE_ASAN)
LSAN_SUPPRESSION_FILEA suppression file for LSAN for false positive memory leaks
LSAN_MALLOC_CONTEXT_SIZELSAN Malloc context size

These cache variables can be set during CMake configuration like so:

    $ cmake --build btc -DUSE_ASAN=1 -DUSE_UBSAN=1 -DASAN_STACK_USE_AFTER_RETURN=1

or enabled after configuration by using ccmake or similar CMake configuration tool. The tests added with simics_add_test() would then have to be executed with ctest for the sanitization to apply.

Keep in mind that these variables will have an affect for all CMake build types (Debug, Release etc).

13.1 ASAN Considerations

ASAN, on average, adds a 2x slowdown. It also adds a RAM overhead, along with longer compilation times. It therefore makes sense to use ASAN when verifying the model's correctness, such as running tests. It might not make as much sense to use ASAN compiled models for the purpose of verifying software that interfaces with the model.

It also makes sense to use high level of compilation optimization when compiling sanitized modules, since that minimizes the slowdown. Compiling with debug information is not mandatory for ASAN to trigger, but is needed to get useful information in the stack trace.

ASAN_STACK_USE_AFTER_RETURN adds even more performance and memory overhead, but as the name suggests can find use of pointers pointing to stack allocated object after a function return. See https://github.com/google/sanitizers/wiki/AddressSanitizerUseAfterReturn for more information.

13.2 Sanitized modules in CLI

If you want to interface with your Simics modules compiled with sanitizers using the Simics CLI, you can run the CMake target simics-asan from within your CMake build directory. Example:

    $ cmake --build bt --target simics-asan

This makes sure LD_PRELOAD and the ASAN options are properly setup before launching Simics, which is a requirement for the modules to trigger on errors correctly.

14 Checking for dead DML methods

Using simics_add_dead_methods_check() on a target will add a post build step that will check if the module contains any dead DML methods. This will only happen if the USE_DML_DEAD_METHODS_CHECK option is enabled during configuration.

By default, the check will only apply to source code that belongs to the current module. However, the simics_add_dead_methods_check() has an argument EXTRA_SOURCES that expands the scope of the dead methods analysis. This argument can be used to scan common code.

Dead DML methods are methods that have been implemented but are never called. One example would be an implemented post_init() method in an attribute, but the attribute never instantiates the post_init template. This would result in a post_init method that is never invoked.