Using Custom CMake Target Properties with VisualGDB
Large CMake-based codebases often use custom macros or functions for defining executables and libraries instead of the regular add_executable() and add_library() statements. This tutorial shows how to configure VisualGDB to automatically recognize those custom statements and automatically edit them in response to adding/removing source files in Solution Explorer or changing properties via the Target Properties window.
We will create a CMake-based project, add 2 CMake functions for defining custom targets using the semantics similar to the ESP-IDF framework and the MySQL codebase and show how to configure VisualGDB to recognize those statements.
Before you begin, install VisualGDB 5.5 or later.
- Start Visual Studio and locate the VisualGDB Linux Project Wizard:
- Specify the name and location for your project:
- On the first page of the Linux Project Wizard select “Application -> CMake -> Use the Advanced CMake Project Subsystem”:
- On the next page of the wizard select your target. In this tutorial we will build the project on the Windows machine using the Raspberry Pi cross-toolchain, however the steps described here will also apply for projects built on the Linux side:Click “Finish” to finish generating the project. VisualGDB will create a basic CMake project with one target.
- Now we will add 4 library targets to the project and will later convert them to use the custom macros. Select “Add->New Item” in Solution Explorer:
- Pick “Static Library” and click “Add“:
- Add 3 more libraries until the main CMakeLists.txt file declares 4 different libraries:
- Create a new text file and save it under <Project Directory>\functions.cmake. Then add the following content to it:
function(register_static_library NAME) add_library(Static${NAME} ${ARGN}) endfunction(register_static_library) function(set_static_library_cflags NAME) target_compile_options(Static${NAME} PRIVATE ${ARGN}) endfunction(set_static_library_cflags) function(register_shared_library) cmake_parse_arguments(_ "" "NAME" "SOURCES;CFLAGS" ${ARGN}) add_library(Shared${__NAME} SHARED ${__SOURCES}) target_compile_options(Shared${__NAME} PRIVATE ${__CFLAGS}) endfunction(register_shared_library)
This defines 3 custom CMake functions:
- register_static_library() will create a new static library with the given name and sources (prepending ‘Static’ to the name)
- set_static_library_cflags() will set the compilation flags for the static library
- register_shared_library() will register a shared library and set the flags for it at the same time
- Include functions.cmake from your main CMakeLists.txt file and replace the direct target definitions with the following lines:
register_static_library(Library1 Library1.cpp) register_static_library(Library2 Library2.cpp) set_static_library_cflags(Library1 -DTESTMACRO) register_shared_library(NAME Library3 SOURCES Library3.cpp CFLAGS -DTESTMACRO) register_shared_library(NAME Library4 SOURCES Library4.cpp)
Note that register_static_library() and register_shared_library() use different syntax. While register_static_library() expects the target name followed by a list of sources, register_shared_library() uses CMake argument groups to separate name, source list and CFLAGS.
- Try opening any source file belonging to the project. VisualGDB will detect that the targets are defined using custom functions or macros and will advise to define a custom target schema:You can disable this warning via Tools->Options->VisualGDB->CMake->Detect Ambiguous Target Definitions.
- Also editing target properties or adding sources to the targets defined with those functions will not work unless a schema is defined:
- If your project structure is more complex than this tutorial shows, you can use the “Step into Target Definition” command to launch VisualGDB’s Cmake debugger:
- This will allow stepping through the CMake files, viewing call stack and variable values just as if it was a regular program:
- Now we will show how to define the custom property schema that will allow VisualGDB to automatically edit the calls to the custom CMake functions. Create a subdirectory called “TargetDefinitions” inside your project directory and add the following files there:
custom.tgt:CustomStaticLibrary match register_static_library() priority 1000 props custom.prop statements custom.stmt
custom.stmt:
register_static_library() from: register_static_library() format: register_static_library(@*) subformat: %name @sources
custom.prop:
@internal target.decl: register_static_library() target.name = target.decl.name target.sources = target.decl.sources
The custom.tgt file defines a rule that will be used for all targets that have ‘register_static_library()’ in their backtrace (i.e. the call stack when the target was registered). It specifies a priority (in case the backtrace has multiple statements matching different targets) and links to properties and statements files.
The custom.stmt file tells VisualGDB how to interpret various statements. In this example, it defines the register_static_library() statement that exposes 2 properties: name (1 token) and sources (0 or more tokens).
Finally, custom.prop file defines one property category (@internal) and 3 properties:- target.decl is defined as an instance of the register_static_library() statement (that internally exposes the name and sources properties per the custom.stmt file).
- target.name and target.sources are special statement names expected by VisualGDB. They are defined as shortcuts to the name and sources subproperties of target.decl.
- Open VisualGDB Project Properties and set the Custom Target Definitions Directory to “TargetDefinitions“:
- Press OK to apply the settings. Locate the StaticLibrary1 target in Solution Explorer and verify that the Matching layout rule for it is CustomStaticLibrary:
- Try adding a new source to the static library:
- VisualGDB will automatically update the register_static_library() statement:This happens because VisualGDB will internally locate the “target.sources” property, confirm that it’s defined in a unique way (i.e. no other target shares the same definition) and will then edit it according to the rules we specified.
- You can now also rename the target (VisualGDB will automatically read and write the “target.name” property we defined):
- Now we will show how to map the set_static_library_cflags() statement to the Visual Studio properties GUI. Add the following text to the schema files:
custom.tgt:CustomStaticLibrary <...> spelling_regex Static(.*)
custom.stmt:
set_static_library_cflags() from: register_static_library().file format: set_static_library_cflags(%target @*) insert: after register_static_library()
custom.prop:
C/C++ General CFLAGS: set_static_library_cflags() help = Compiler Flags for this target
This defines a new statement called set_static_library_cflags(). VisualGDB will search the CMakeLists.txt where the register_static_library() was called for the target for all set_static_library_cflags() statements where the first argument matches the spelling name of the target (that is defined by stripping the “Static” prefix via the spelling_regex property). If none are found, VisualGDB will insert one right after the register_static_library() call.
- Reopen the project and open Visual Studio properties for the target. The custom CFLAGS property will now appear there:
- Select multiple targets at once and set CFLAGS to -DANOTHERMACRO:
- See how VisualGDB edited the first set_static_library_cflags() statement and inserted another one for Library2:
- As the CFLAGS property typically contains many different compiler options, you can define flag filters to display them in a reasonable way. Replace the C/C++ group contents in custom.prop with the following code:
C/C++ General CFLAGS[self.cflags]: set_static_library_cflags() help = Compiler Flags for this target Preprocessor Definitions[cc.defines] -> self.cflags help = Defines a preprocessing symbols for the source files type = list prefix = -D pretoken = -D
This will move all -D<MACRO> and -D<space><MACRO> flags into a separate property called “Preprocessor Definitions”:
- VisualGDB comes with predefined filters for common flags used by popular compilers, so you can replace the entire custom.prop file with the following code instead:
@internal target.decl: register_static_library() target.name = target.decl.name target.sources = target.decl.sources self.cflags: set_static_library_cflags() %include $(VISUALGDB_DIR)\rules\PropertyEngine\common\cflags.prop
This will result in a regular-looking set of properties for the target:
- Editing them will transparently modify the set_static_library_cflags() statements so you won’t need to interpret the flags manually:
- The register_shared_library() statement combines sources and CFLAGS in the same statement, so the schema for it will look slightly different. Create/update the schema files as shown below:
custom.tgt:CustomSharedLibrary match register_shared_library() priority 1000 props shared.prop statements custom.stmt
custom.stmt:
CustomSharedLibrary match register_shared_library() priority 1000 props shared.prop statements custom.stmt
shared.prop (note that it’s a separate file from custom.prop):
@internal target.decl: register_shared_library() target.name = target.decl.NAME target.sources = target.decl.SOURCES self.cflags = target.decl.CFLAGS %include $(VISUALGDB_DIR)\rules\PropertyEngine\common\cflags.prop
- Now you can use the same convenient GUI to edit the properties of shared libraries, or add files to them and VisualGDB will know how to edit the register_shared_library() statements:You can find detailed reference for the CMake statement definition syntax in the <VisualGDB Directory>\Rules\PropertyEngine\CMake\cmake.stmt file as well as the schema used by VisualGDB to edit regular CMake targets and files.
- Finally we will show how to add custom target templates for your project. Go to the custom target definitions directory and create a templates/target/MyCustomTarget subdirectory with the following contents:
- template.xml:
<?xml version="1.0"?> <CMakeTargetTemplate xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <Name>Sample Custom Target Template</Name> <FileNameBase>MyCustomTarget</FileNameBase> <CMakeStatementTemplate>register_static_library($$TARGETNAME$$ $$TARGETSOURCES$$)</CMakeStatementTemplate> <SourceFileTemplates> <string>$$TARGETNAME$$.cpp</string> </SourceFileTemplates> <ExtraFileTemplates> <string>$$TARGETNAME$$.h</string> </ExtraFileTemplates> <Icon>wizard.png</Icon> </CMakeTargetTemplate>
- $$TARGETNAME$$.cpp:
#include <stdio.h> void Sample$$TARGETNAME$$Function() { printf("Hello from $$TARGETNAME$$!\n"); }
- $$TARGETNAME$$.h:
#pragma once void Sample$$TARGETNAME$$Function();
- wizard.png:
(any 32×32 icon, e.g. this one)
- template.xml:
- Right-click on the project node in Solution Explorer and select Add->New Item. VisualGDB will show “Sample Custom Target Template” after the regular target templates:Selecting the custom template will insert the contents of the CMakeStatemenTtemplate into CMakeLists.txt in the selected folder (if a new folder was selected, it will be automatically included from your main CMakeLists.txt). It will also create the source and other files based on the template. The only difference between SourceFileTemplates and ExtraFileTemplates is that the extra files won’t be included in the $$TARGETSOURCES$$ variable.
The project shown in this tutorial is available on our Github repository.