Guidelines and HOWTOs/CMake/Library

From KDE Community Wiki
Revision as of 14:00, 15 July 2017 by Frinring (talk | contribs) (add NO_POLICY_SCOPE)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This tutorial will walk through how the build system for a KDE Framework is constructed. It is intended to helpful both for those who wish to contribute to KDE Frameworks and for those who just want to know "best practices" for making a library with a CMake-based buildsystem.

We will use KArchive as our example. You can view the KArchive source directly by cloning git://anongit.kde.org/karchive or by browsing the repository online. Note that this tutorial may deviate from the actual source code in some places.

Note

This tutorial assumes that you are familiar with using CMake for a straightforward application. If not, you should look at the other tutorials.


CMakeLists.txt

The main CMakeLists.txt file starts in the usual way:

cmake_minimum_required(VERSION 2.8.12)
project(KArchive)

After that, we search for the dependencies we require:

include(FeatureSummary)
find_package(ECM 5.12.0 NO_MODULE)
set_package_properties(ECM PROPERTIES
    DESCRIPTION "Extra CMake Modules"
    URL "https://projects.kde.org/projects/kdesupport/extra-cmake-modules"
    TYPE REQUIRED
)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})

set(REQUIRED_QT_VERSION 5.2.0)
find_package(Qt5Core ${REQUIRED_QT_VERSION} REQUIRED NO_MODULE)
set_package_properties(Qt5Core PROPERTIES
    DESCRIPTION "Core library of the Qt5 framework"
    URL "http://www.qt.io"
    TYPE REQUIRED
)

find_package(ZLIB)
set_package_properties(ZLIB PROPERTIES
    DESCRIPTION "Implementation of the deflate compression algorithm"
    URL "http://www.zlib.net"
    TYPE REQUIRED
    PURPOSE "Support for gzip compressed files and data streams"
)

find_package(BZip2)
set_package_properties(BZip2 PROPERTIES
    DESCRIPTION "Implementation of the BZip2 compression algorithm"
    URL "http://www.bzip.org"
    TYPE RECOMMENDED
    PURPOSE "Support for BZip2 compressed files and data streams"
)

find_package(LibLZMA)
set_package_properties(LibLZMA PROPERTIES
    DESCRIPTION "Implementation of the LZMA2 compression algorithm"
    URL "http://tukaani.org/xz/"
    PURPOSE "Support for xz and 7-Zip compressed files and data streams"
)

feature_summary(
    WHAT REQUIRED_PACKAGES_NOT_FOUND
    FATAL_ON_MISSING_REQUIRED_PACKAGES
)

We use the REQUIRED_QT_VERSION variable for two reasons: firstly, it allows the required Qt version to be synchronized between the call to find Qt5Core in this file and the call to find Qt5Test in the autotests CMakeLists.txt; secondly, it makes it easier to script updates the required version across multiple projects (particularly important for the KDE Frameworks).

Being a KDE Framework, KArchive naturally uses the standard settings modules for KDE software. Note the use of KDEFrameworkCompilerSettings, rather than KDECompilerSettings. This includes more stringent compiler and Qt API checks to ensure KArchive can be used in projects that also make use of those checks. The NO_POLICY_SCOPE is needed for some of the settings to also take effect in the project.

include(KDEInstallDirs)
include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE)
include(KDECMakeSettings)

After that, we include some modules designed to help with writing a library. We will explain their purpose when we use the functions they provide.

include(GenerateExportHeader)

include(ECMGenerateHeaders)
include(ECMGeneratePriFile)
include(ECMPackageConfigHelpers)
include(ECMSetupVersion)

The first one of those helpers that we will use is the one provided by ECMSetupVersion. This allows all the version information for the library to be constructed in a single place: setting variables for use in later CMake code, creating a C++ header file with compile-time version information macros and creating a ConfigVersion.cmake file for find_package to use.

It is important that this happens before including subdirectories, otherwise those subdirectories will not have access to the version variables that are set by this function.

set(KF5_VERSION "5.13.0") # handled by release scripts

ecm_setup_version(${KF5_VERSION}
    VARIABLE_PREFIX KARCHIVE
    VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/karchive_version.h"
    PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5ArchiveConfigVersion.cmake"
    SOVERSION 5
)

install(
    FILES ${CMAKE_CURRENT_BINARY_DIR}/karchive_version.h
    DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}
    COMPONENT Devel
)

Again, we use a separate version variable to help script updates.

Note

If your project requires CMake 3.0 or later, you should specify the version in the project command:
cmake_minimum_required(VERSION 3.0)
project(KArchive
    VERSION 5.13.0
)

and use the alternative form of ecm_setup_version, which will use the version passed to project:

ecm_setup_version(PROJECT
    VARIABLE_PREFIX KARCHIVE
    VERSION_HEADER "${CMAKE_CURRENT_BINARY_DIR}/karchive_version.h"
    PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF5ArchiveConfigVersion.cmake"
    SOVERSION 5
)

install(
    FILES ${CMAKE_CURRENT_BINARY_DIR}/karchive_version.h
    DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}
    COMPONENT Devel
)


Now we include the subdirectories that contain project code. KDE Frameworks use the src subdirectory for the main body of source code, autotests for any tests that can be run automatically, such as with make test, and tests for any tests that are intended for developers to run manually (common in GUI modules).

add_subdirectory(src)
if (BUILD_TESTING)
    add_subdirectory(autotests)
    add_subdirectory(tests)
endif()

Once the src subdirectory has defined and installed the library targets, we need to provide a way for other CMake projects to find them. This is done with a package config file set, which consists of three files. KF5ArchiveConfigVersion.cmake (generated by ecm_setup_version above) will allow find_package(KF5Archive 5.12.0) to determine whether this is a compatible library version. KF5ArchiveConfig.cmake will provide all the information necessary to actually use KArchive. Most of this information will be generated by CMake and put in KF5ArchiveTargets.cmake.

First, we decide where the package config files will be installed. For libraries, the recommended installation directory is in the cmake/<package_name> subdirectory of the location the library files are installed to; KDEInstallDirs takes care of everything but the last component.

set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF5Archive")

Now we generate KF5ArvhieConfig.cmake. The template file is shown below. ecm_configure_package_config_file behaves like configure_file, except that it additionally replaces @PROJECT_INIT@ with some code that defines useful helper macros and variables.

ecm_configure_package_config_file(
    "${CMAKE_CURRENT_SOURCE_DIR}/KF5ArchiveConfig.cmake.in"
    "${CMAKE_CURRENT_BINARY_DIR}/KF5ArchiveConfig.cmake"
    INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}
)

We install the files we generated and tell CMake to generate and install KF5ArchiveTargets.cmake, which will be included by KF5ArchiveConfig.cmake and provide the target information other projects need to use KArchive.

install(
    FILES
        "${CMAKE_CURRENT_BINARY_DIR}/KF5ArchiveConfig.cmake"
        "${CMAKE_CURRENT_BINARY_DIR}/KF5ArchiveConfigVersion.cmake"
    DESTINATION "${CMAKECONFIG_INSTALL_DIR}"
    COMPONENT Devel
)

install(
    EXPORT KF5ArchiveTargets
    DESTINATION "${CMAKECONFIG_INSTALL_DIR}"
    FILE KF5ArchiveTargets.cmake
    NAMESPACE KF5::
)

The final install(EXPORT) command includes a NAMESPACE specification of KF5::. This means that projects using KArchive will use target names prefixed with KF5:: for KArchive's targets; in particular, they will link against KF5::Archive.

Finally, we have the usual feature_summary call to tell the user about what packages were looked for and which were found.

feature_summary(
    WHAT ALL
    FATAL_ON_MISSING_REQUIRED_PACKAGES
)

KF5ArchiveConfig.cmake.in

This file provides all the information another CMake-based project's buildsystem might need to use KArchive. @PACKAGE_INIT@ is replaced with useful macros and variables. We only use one here: find_dependency, which is a wrapper around find_package that behaves properly when run from within another find_package call.

Note that variables describing the build-time configuration of KArchive are set: these can be useful for downstream projects to decide which of their own features to enable or disable.

@PACKAGE_INIT@

find_dependency(Qt5Core @REQUIRED_QT_VERSION@)

set(KArchive_HAVE_BZIP2 "@BZIP2_FOUND@")
set(KArchive_HAVE_LZMA  "@LIBLZMA_FOUND@")

include("${CMAKE_CURRENT_LIST_DIR}/KF5ArchiveTargets.cmake")

src/CMakeLists.txt

The CMakeLists.txt file for the library code has a format that should be mostly familiar.

Note that some of the calls have been re-ordered from the original KArchive code, in order to make the discussion easier.

set(karchive_OPTIONAL_INCLUDES)
set(karchive_OPTIONAL_LIBS)
set(karchive_OPTIONAL_SRCS)

set(HAVE_BZIP2_SUPPORT ${BZIP2_FOUND})
if (BZIP2_FOUND)
    if (BZIP2_NEED_PREFIX)
        set(NEED_BZ2_PREFIX 1)
    endif()

    set(karchive_OPTIONAL_INCLUDES ${karchive_OPTIONAL_INCLUDES} ${BZIP2_INCLUDE_DIR})
    set(karchive_OPTIONAL_LIBS ${karchive_OPTIONAL_LIBS} ${BZIP2_LIBRARIES})
    set(karchive_OPTIONAL_SRCS ${karchive_OPTIONAL_SRCS} kbzip2filter.cpp)
endif()

set(HAVE_XZ_SUPPORT ${LIBLZMA_FOUND})
if (LIBLZMA_FOUND)
    set(karchive_OPTIONAL_INCLUDES ${karchive_OPTIONAL_INCLUDES} ${LIBLZMA_INCLUDE_DIRS})
    set(karchive_OPTIONAL_LIBS ${karchive_OPTIONAL_LIBS} ${LIBLZMA_LIBRARIES})
    set(karchive_OPTIONAL_SRCS ${karchive_OPTIONAL_SRCS} kxzfilter.cpp k7zip.cpp)
endif()

configure_file(config-compression.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-compression.h)

set(karchive_SRCS
    karchive.cpp
    kar.cpp
    kcompressiondevice.cpp
    kfilterbase.cpp
    kfilterdev.cpp
    kgzipfilter.cpp
    klimitediodevice.cpp
    knonefilter.cpp
    ktar.cpp
    kzip.cpp
    krcc.cpp
)

add_library(KF5Archive ${karchive_SRCS} ${karchive_OPTIONAL_SRCS})

So far, so normal. Recall from the description of the install(EXPORT) call in the main CMakeLists.txt file that projects using KArchive would link against KF5::Archive. The convention is CMake is that imported targets (from find_package calls) have :: in their names, and targets from the current build system do not. This is why we used the target name KF5Archive in the add_library call above.

In order to allow for more consistent code, and ease writing examples in the documention, we define an alias:

add_library(KF5::Archive ALIAS KF5Archive)

This allows the same target name (KF5::Archive) to be linked to in both code withing the KArchive project and external code. The KF5::Archive target cannot be modified directly, however, so we continue to use KF5Archive in the rest of this file.

When using dynamic libraries, making sure the right symbols are available can be a non-trivial task. On DLL-based platforms, such as Windows, each symbol or group of symbols needs to be explicitly exported (when building the DLL) and imported (when using it). KDEFrameworkCompilerSettings ensures that the same is true on ELF-based platforms (like Linux). generate_export_header is used to generate a header that provides macros for doing this.

generate_export_header(KF5Archive BASE_NAME KArchive)

You are probably familiar with Qt's classname forwarding headers: rather than doing #include <QtCore/qstring.h>, you can use the class name as the header, doing #include <QtCore/QString>. A similar feature is provided by KDE Frameworks, and the ecm_generate_headers function simplifies this work by generating the classname headers, assuming the original header names follow a standard naming pattern of <lowercase classname>.h.

ecm_generate_headers(KArchive_HEADERS
    HEADER_NAMES
        KArchive
        KArchiveEntry
        KArchiveFile
        KArchiveDirectory
        KAr
        KCompressionDevice
        KFilterBase
        KFilterDev
        KTar
        KZip
        KZipFileEntry
    REQUIRED_HEADERS KArchive_HEADERS
)
if (LIBLZMA_FOUND)
    ecm_generate_headers(KArchive_HEADERS
        HEADER_NAMES
        K7Zip
        REQUIRED_HEADERS KArchive_HEADERS
    )
endif()

It would be convenient for our users if linking against KF5::Archive automatically set up the correct include path - this is what happens when you link against Qt5::Core for example. The next step does just that.

CMake maintains two lists of include directories for the KF5Archive target: PRIVATE directories are available to KF5Archive when it is being built, and INTERFACE directories are available to other code that links to KF5Archive. For convenience, it is also possible to specify PUBLIC directories, which will be added to both lists. We can use the INTERFACE list for the installed header location and the PRIVATE list for include paths used only by the implementation of KF5Archive.

We use generator expressions (as detailed in the target_include_directories command documentation) to further limit the use of the installed include path until after the target has been installed (this path will be put into KF5ArchiveTargets.cmake, but not use by the KArchive project internally).

target_include_directories(KF5Archive
    INTERFACE
        "$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF5}/KArchive>"
    PRIVATE
        ${ZLIB_INCLUDE_DIR}
        ${karchive_OPTIONAL_INCLUDES}
)

CMake maintains the same sort of twin lists for link dependencies as for include directories. For KF5Archive, Qt5Core is used in the public API of the library (KF5Archive's public headers include Qt5Core header files and use Qt5Core classes), so Qt5::Core should be on the INERFACE list for KF5Archive (as well as on the PRIVATE list, because it is also needed to build KF5Archive). ZLIB, on the other hand, is only used in the implementation of KF5Archive, and so only needs to be on the PRIVATE list.

target_link_libraries(KF5Archive
    PUBLIC
        Qt5::Core
    PRIVATE
        ${karchive_OPTIONAL_LIBS}
        ${ZLIB_LIBRARY}
)

Libraries, particularly on ELf-based platforms, need version information associated with them. This determines the name of the shared object file and any links to it (eg: libKF5Archive.so, libKF5Archive.so.5 and libKF5Archive.so.5.13.0). This is done with the VERSION and SOVERSION target properties. We also set the export name to Archive. This ensures that the target will appear in other projects as KF5::Archive rather than KF5::KF5Archive. Your own library may not need to set this property.

set_target_properties(KF5Archive PROPERTIES
    VERSION     ${KARCHIVE_VERSION}
    SOVERSION   ${KARCHIVE_SOVERSION}
    EXPORT_NAME "Archive"
)

Now we install the library. The KF5_INSTALL_TARGETS_DEFAULT_ARGS variable contains the arguments necessary for install(TARGETS) to install the various bits of the library in the correct locations (eg: on DLL platforms, the .dll file may need to go in a different location to the .lib file). Note the EXPORT argument, though - this corresponds to the name given in the install(EXPORT) command in the main CMakeLists.txt file, and ensures this target will be provided by the KF5ArchiveTargets.cmake file.

install(
    TARGETS KF5Archive
    EXPORT KF5ArchiveTargets
    ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}
)

Installation of the headers is straightforward. We separate out the headers in the Devel component for the convience of packages; it is common in Linux distributions to ship the runtime parts of a library separately from the parts needed to build against it.

install(
    FILES
        ${CMAKE_CURRENT_BINARY_DIR}/karchive_export.h
        ${KArchive_HEADERS}
    DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KArchive
    COMPONENT Devel
)

Finally, we generate the file needed to use KArchive from qmake.

ecm_generate_pri_file(
    BASE_NAME KArchive
    LIB_NAME KF5Archive
    DEPS "core"
    FILENAME_VAR PRI_FILENAME
    INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KArchive
)
install(
    FILES ${PRI_FILENAME}
    DESTINATION ${ECM_MKSPECS_INSTALL_DIR}
)

autotests/CMakeLists.txt

For completeness, the autotests CMakeLists.txt is listed here, although we will not discuss it. This file is provided for completeness, but readers of this tutorial are expected to be familiar with the code it contains.

remove_definitions(-DQT_NO_CAST_FROM_ASCII)

include(ECMAddTests)

find_package(Qt5Test ${REQUIRED_QT_VERSION} CONFIG QUIET)
set_package_properties(Qt5Core PROPERTIES
    DESCRIPTION "Qt5 unit testing framework"
    URL "http://www.qt.io"
    TYPE OPTIONAL
    PURPOSE "Required to build the unit tests"
)

if(NOT Qt5Test_FOUND)
    message(STATUS "Qt5Test not found, autotests will not be built.")
    return()
endif()

ecm_add_tests(
    karchivetest.cpp
    kfiltertest.cpp
    deprecatedtest.cpp
    LINK_LIBRARIES KF5::Archive Qt5::Test
)

target_compile_definitions(deprecatedtest PRIVATE KARCHIVE_DEPRECATED=)
target_link_libraries(kfiltertest ${ZLIB_LIBRARIES})

########### klimitediodevicetest ###############

ecm_add_test(
    klimitediodevicetest.cpp
    ../src/klimitediodevice.cpp
    TEST_NAME klimitediodevicetest
    LINK_LIBRARIES Qt5::Test
)
target_include_directories(klimitediodevicetest
    PRIVATE $<TARGET_PROPERTY:KF5Archive,INTERFACE_INCLUDE_DIRECTORIES>)

tests/CMakeLists.txt

This file is provided for completeness, but readers of this tutorial are expected to be familiar with the code it contains.

remove_definitions(-DQT_NO_CAST_FROM_ASCII)

include(ECMMarkAsTest)

macro(karchive_executable_tests)
    foreach(_testname ${ARGN})
        add_executable(${_testname} ${_testname}.cpp) # TODO NOGUI
        target_link_libraries(${_testname} KF5::Archive)
        ecm_mark_as_test(${_testname})
    endforeach(_testname)
endmacro()

karchive_executable_tests(
    ktartest
    krcctest
    kziptest
)

if(LIBLZMA_FOUND)
    karchive_executable_tests(
        k7ziptest
    )
endif()