Build with CMake, part 2

Find and Link Libraries with CMake

By Chris Rizzitello

In Part 1 of our series on CMake, an open-source, cross-platform family of tools designed to build, test and package software, I provided an overview and showed you how to build a basic CMake project. In today’s Part 2, I’ll show you how to find libraries and link them to your application.

When working on almost any code project you’ll probably want to use another library. There are several kinds of libraries you may need to use. Here’s a look at different types of libraries, and insight on how to use them within an example CMake project.

Libraries can either be compiled within your source code (embedded) or made available by the system as a so-called “system library.” There’s are the most common types of libraries:

Embedded libraries, which embed their resulting code in the final binary, whether that’s an application or another library.

Source-only libraries, which are meant to be compiled within your code.

System libraries, which are libraries on your computer system. Along with the library you will have headers or a .lib file to use when including files from the lib.

How CMake Handles Different Types of Libraries

Embedded libraries

These are the easiest to work with: just copy them into your source tree and add them to your project as if they were any other source file. Any Library that is a single header is this kind of library an example of one would be Json.h. Consider placing them in a sub directory to keep your source tree clean.

Source-only libraries as part of the main application

For this type method you only need to copy the source and add them to the list of sources for the target. When you use the object, you need to use a normal include statement that includes the header via a relative path. For this, I’ll use a “numbers” library as an example:

Ex:  Folder Structure

exapp/main.cpp

exapp/CMakeList.txt

exapp/numbers/numbers.cpp

exapp/numbers/numbers.h 

Ex: CMakeList.txt

add_executable(sampleApp
main.cpp
            numbers/numbers.cpp
            numbers/numbers.h
)

Ex: Including the library main.cpp 

  #include "numbers/numbers.h" 

This will build the library into our example application and will not require linking to any new libraries other than the libraries numbers needs to build. For our example, numbers has no library dependencies.

Source-only libraries as a separate library

You have the option to build this as a separate static (or shared) library and link it to your application. This provides some benefits, such as:

  • Reducing compilation time while developing. (As long as you do not touch the library’s code, you only need to compile it once.)
  • You can re-use the library for different parts of the project without recompiling. (For example you could use the library in your units tests by simply linking to it.)
  • More easily keep the code well-structured.

Using the same library as before, let's look at what we can do to make “numbers” its own library. The first thing to do is start to clean up the main CMakeLists.txt. We can accomplish this by removing the numbers source files from our target and instead use add_subdirectory to add the numbers directory. This will look for a CMakeLists.txt file in that directory so we will write that next. 

We also need to link our target to the library that we are going to create. To do this, we call target_link_libraries. Libraries can be linked PUBLIC, PRIVATE or INTERFACE. 

  • PUBLIC: Used by the library and its consumers.
  • PRIVATE: Only needed by the library.
  • INTERFACE: Not used by the library but needed for consumers.

After the edits our main CMakeLists.txt will look like this: 

  cmake_minimum_required(VERSION 3.5)
  project(numConvert LANGUAGES CXX)
  add_subdirectory(numbers)
  add_executable(numConvert main.cpp)
  target_link_libraries(numConvert PRIVATE numbers)

Inside the numbers directory we need to create a new CMakeLists.txt that will contain instructions for building our library. It only needs to have a single add_library call. This is how we make library target:

   add_library(numbers numbers.cpp numbers.h)

This will build our library into the target as a static library. We use the same “include” statement for numbers used in the previous example. When we talk later about creating shippable libraries, we’ll explore methods that can be used to modify the way we include the library headers. 

(Note: You can use the same setup for other libraries that you plan to include, as long as they already have a CMake configuration. However it’s not always ideal to build everything. One more note: embedded dynamic (shared) libraries have a similar approach, but this is a far more advanced concept we will cover in a future blog.)

System libraries

This is going to be the most common way you will use libraries. System libraries normally provide not only a way to tell you where they are but also how to properly use them. For instance, they let you know whether you need other libraries, flags required to link, etc. The library also typically provides this information in a file. Of course, CMake can provide .cmake files, but it can also handle other methods like pkg-config (.pc) files. 

For the purposes of this blog, I’ll focus on the first case. The main thing we will use here is find_package. Most simply, we can do something like find_package(zlib REQUIRED) and zlib will need to be found before we can build our project. 

After configuration, we can use libraries and link libraries from it to our target. This requires that the package you’re looking for is already built and installed. If CMake can not find zlib then you can not build the project. In some situations, you may want to add the HINTS option to your call to provide a “hint” as to where Cmake should look for the findZlib.cmake file. 

To link, you can add zlib to your list. This will not ship the library when deploying your application. I’ll talk more on this topic later in our blog series. 

When find_package is called, CMake checks several directories for a find<PACKAGENAME>.cmake or <PACKAGE>Config.cmake. When found it will be used to set all the internal variables that Cmake will use for the library. 

Package search starts in the CMAKE_PREFIX_PATH and will check several directories from here. The `Package Config` search will check some additional directories, as well. In the case that CMake is not able to find the package, you will need to provide an option of <PACKAGE>_DIR when configuring to help find it. 

In addition, you can use the version number and HINTS in the find_package call to limit the search to a version compatible with the version you provided, and to also check the paths listed as hints in addition to the normal search path. 

If we planned to include a copy of zlib 1.2.11 on our system but had not yet installed it, in our CMakeLists.txt we could use:

find_package(zlib 1.2.11 REQUIRED HINTS C:\zlib\lib\cmake)

Or if we know exactly where it is we can use the configure option -DZLIB_ROOT=C:\zlib to force it to look in that path for the “ROOT” of the zlib install. 

The FindZlib.cmake module is part of CMake. It is often useful to check the CMake script that will be finding the package so you have an idea of the targets it creates. This makes it easier to link the package. In this case ZLIB::ZLIB is the target name we would use for linking and we see that some other variables like ZLIB_FOUND are also set by the FindZlib.cmake module. 

CMake provides a few of its own Find modules. If your project is not one of those for which CMake provides a find module, you must provide one with your library. 

A Word About Qt

I often use Qt with my projects. Since the release of Qt 5 in 2012, CMake has been an option. And since Qt 6’s release in 2020, it has become the preferred method of making a Qt project. 

The first thing you should know about using Qt with CMake are the different ways to find Qt: 

Use find_package(Qt5 … ) to find Qt 5 

Use find_package(Qt6… )to find Qt 6 

Use these two calls together to find a version of Qt:

find_package(QT NAMES Qt6 Qt5 …)

find_package(Qt${QT_VERSION_MAJOR} ….)

There is one important variable that I always set in my Qt projects. QT_DEFAULT_MAJOR_VERSION will set the version of Qt that will be used for any versionless commands. It also will enforce the version as being Qt5 or Qt6. 

You’re probably thinking that find_package will completely take care of this for you, but that’s not entirely true with Qt. If you're using QtCreator you’re using a kit along with your CMake configuration. This kit is setting up all the Qt stuff, as well as some system info like compilers, debuggers, etc. When you have Both Qt5 and Qt6 and are using some tools that set their own CMake variables (like Qt Creator or VSCode) you can have some oddities where when using a Qt 6 kit you use some Qt 5 parts.

To remove any chance of this happening on a system with both Qt5 and Qt6, I set this variable. The nice thing about this is that find_package for Qt can be changed into: 

 find_package (Qt${QT_DEFAULT_MAJOR_VERSION}  … )   and this will find the version of Qt I want just by setting the QT_DEFAULT_MAJOR_VERSION. 

It also allows someone to use either Qt 5 or Qt 6 to build the project. This can be useful in a variety of situations, such as in the process of porting from Qt 5 to Qt 6, or when having to support both Qt 5 and Qt 6 due to a requirement to have a platform that is currently still only using Qt 5.

Let's Talk Components

A lot of real-world projects are a set of smaller libraries. Each of the libraries in CMake is a component. This structure means you only have to worry about finding and linking the parts you need from larger projects. Qt is a project made of many components. If you have used qmake before you have seen and used the Qt components, which qmake called Qt Modules. Where you would have set  QT+= in qmake, instead you will use components in CMake.

Each Qt Modules Document page will tell you how to find it for both qmake and CMake. If the project will use Qt Widget, change the find_package call to look like this:

find_package(Qt${QT_DEFAULT_MAJOR_VERSION} REQUIRED COMPONENTS Widgets)

Linking to widgets is also easy. Use this: 

target_link_library(Foo Public Qt::Widgets)

You can add more than one component in a single call or add additional components in separate calls. Just keep in mind that if your target needs a component you must find it either directly or as a dependency of a component you have found. 

Example: CMakeLists.txt

The example code here comes from Qt Creator when you hit “new widget project” and choose the CMake build system. For our example, we will have no C++ changes from this code. 

cmake_minimum_required(VERSION 3.5)

project(untitled VERSION 0.1 LANGUAGES CXX)

set(CMAKE_AUTOUIC ON)  
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)

set(PROJECT_SOURCES
    main.cpp
    mainwindow.cpp
    mainwindow.h
    mainwindow.ui
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
qt_add_executable(untitled
    MANUAL_FINALIZATION
    ${PROJECT_SOURCES}
)
# Define target properties for Android with Qt 6 as:
# set_property(TARGET untitled APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
#             ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
if(ANDROID)
    add_library(untitled SHARED
        ${PROJECT_SOURCES}
    )
# Define properties for Android with Qt 5 after find_package() calls as:
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
else()
    add_executable(untitled
        ${PROJECT_SOURCES}
    )
endif()
endif()

target_link_libraries(untitled PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

set_target_properties(untitled PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)

install(TARGETS untitled
BUNDLE DESTINATION .
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR})

if(QT_VERSION_MAJOR EQUAL 6)
qt_finalize_executable(untitled)
endif()

There are a lot of new cmake items in this file. Starting near the top, you will notice the: 

set(CMAKE_AUTOUIC ON)  
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

These are Qt-specific variables that provide some hints for CMake as to what to do when it sees some of the Qt-specific files or objects that need preprocessing before they can be built into the target. 

Setting CMAKE_AUTOMOC ON will make it so CMake will automatically add a moc call for any QObject or item that needs it, and include the moc_ header with the object. Setting CMAKE_AUTORCC ON will ensure that any qrc file added has rcc called on it and that the results are used as source. 

Finally, setting CMAKE_AUTOUIC ON will make it so any ui file added  has uic called and the results are used as source. These should be on for any Qt project since not turning them on will result in the need to manually add any desired calls.

Moving on with our example, we now have these two lines:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

They ensure our compiler will support C++17  – and its support is not optional. Attempts to configure this project with a compiler that doesn’t support C++17 will fail to configure.

We then see this: 

qt_add_executable(untitled
    MANUAL_FINALIZATION
    ${PROJECT_SOURCES}
)

What does this qt_add_executable call do that add_executable does not ? It’s all about that MANUAL_FINALIZATION call. This will ensure that if the target needs to be finalized at a time after building, it will happen. (this is the default if you are using Cmake >=3.19).

If you have MANUAL_FINALIZATION you must have a matching  qt_finalize_executable() call. We can see this as the last line in the example. 

The next new bit of CMake including all the Android stuff is well commented so I will skip over that and instead look at:

set_target_properties(untitled PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER my.example.com
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
MACOSX_BUNDLE TRUE
WIN32_EXECUTABLE TRUE
)

This will set the properties of the target untitled. There are a lot of properties that you can set so each has to be explicitly added to the list. 

All the MACOSX-BUNDLE_FOO calls set properties of the AppBundle on Mac so that is what you would see under the details of Untitled.App.

We know that an Appbundle will be made because the MACOSX_BUNDLE property is set to TRUE . We also see a Property WIN32_EXECUTABLE is set TRUE. On Windows this will set the application’s entry point to that of a WIN32 Gui application and not a Console application. 

The install has a new Target type BUNDLE we have not seen before that just tells CMake where to Install the Bundle That is the .app created on macOS. 

Looking Ahead

In the next installment in this series, we’ll explore how to make a simple library and install it into the system using CMake. If you missed part 1, read it here.