Guidelines and HOWTOs/CMake/Library
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.
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.
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()