its time to do cmake right
Enough preambles. Does this look familiar to you?
find_package(Boost 1.55 COMPONENTS asio)
list(APPEND INCLUDE_DIRS ${BOOST_INCLUDE_DIRS})
list(APPEND LIBRARIES ${BOOST_LIBRARIES})
include_directories(${INCLUDE_DIRS})
link_libraries(${LIBRARIES})
include和link是路径级别的,这个路径以下的所有目标都要遵循这个规则,这个太大了,会污染其他目标的规则
Don’t. Just don’t. This is wrong in so many dimensions. You are just blindly throwing stuff into a pot of include directories and compiler ags. There is no structure. There is no transparency. Not to mention that functions like include_directories work at the directory level and apply to all entities dened in scope.
And this isn’t even the real problem, what do you do with transitive dependencies? What about the order of linking? Yes, you need to take care about that yourself. The moment you need to deal with the dependencies of your dependencies is the moment your life needs to be reevaluated.
现代的cmake基于targets,为每个targets设定属性
Targets and Properties
Modern CMake is all about targets and properties.
Targets model the components of you application. An executable is a target, a library is a target. Your application is built as a collection of targets that depend on and use each other.
Targets have properties. Properties of a target are the source les it’s built from, the compiler options it requires, the libraries it links against. In modern CMake you create a list of targets and dene the necessary properties on them.
注意指定是private还是interface,这个选项是给谁用的
Build Requirements vs Usage Requirements
INTERFACE and PRIVATE,一个是内部编译选项,一个是外部使用选项
Target properties are dened in one of two scopes: INTERFACE and PRIVATE. Private properties are used internally to build the target, while interface properties are used externally by users of the target. In other words, interface properties model usage requirements, whereas private properties model build requirements of targets.
Interface properties have the prex, wait for it, INTERFACE_ prepended to them.
典型的文件夹结构应该是这样
libjsonutils
├── CMakeLists.txt
├── include
│ └── jsonutils
│ └── json_utils.h--------外部用的头文件
├── src
│ ├── file_utils.h----内部用的头文件
│ └── json_utils.cpp
└── test
├── CMakeLists.txt
└── src
└── test_main.cpp
cmake_minimum_required(VERSION 3.5)
project(libjsonutils VERSION 1.0.0 LANGUAGES CXX)
Nothing surprising here. The rst step is to create our library target:
add_library(JSONUtils src/json_utils.cpp)
Now let’s dene some properties on our target. Why not start with the include directories?
target_include_directories(JSONUtils
PUBLIC
$<INSTALL_INTERFACE:include>------库安装好后直接在include中
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>----在构建过程中是在这个include中
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src
)
Our headers are located in two different places: inside src/ , which contains a utility header called file_utils.h , and in include/ , where our public header json_utils.h lives. For building our library
we need all headers in both locations ( json_utils.cpp includes both), so INCLUDE_DIRS must contain src/ , as well as include/ .
On the other hand, users of jsonutils only need to know about the location of the public header
json_utils.h , so INTERFACE_INCLUDE_DIRS only needs to contain include/ , but not src/ .
There is still a problem, though. While building jsonutils, include/ is at /home/pablo/libjsonutils/include/ , but after installing our library, it will be under ${CMAKE_INSTALL_PREFIX}/include/ . Therefore, the location of this directory needs to be different generator
expressions (https://cmake.org/cmake/help/v3.5/manual/cmake-generator-expressions.7.html)
depending on the situation.
depending on whether we are building or installing the library. To solve this problem, we use
不要用CMAKE_CXX_FLAGS
Leave CMAKE_CXX_FLAGS Alone
不要手动指定标准参数,指定一些属性就OK!
target_compile_options(JSONUtils PRIVATE -Werror)
target_compile_features(JSONUtils PRIVATE cxx_std_11)
Note that there is no reason to manually append -std=c++11 to CMAKE_CXX_FLAGS, let CMake do that for you! Stay away from variable land, model your requirements via properties.
Model dependencies with target_link_libraries
In CMake, target_link_libraries is used to model dependencies between targets.
find_package(Boost 1.55 REQUIRED COMPONENTS regex)
find_package(RapidJSON 1.0 REQUIRED MODULE)
target_link_libraries(JSONUtils
PUBLIC Boost::boost RapidJSON::RapidJSON
PRIVATE Boost::regex
)
Dependencies (a.k.a link libraries) of a target are just another property and are dened in an INTERFACE or PRIVATE scope. In our case, both rapidjson and boost optional (dened in the target Boost::boost ) have to be interface dependencies and be propagated to users, as they are used in a public header that’s imported by clients.
This means that users of JSONUtils don’t just require JSONUtil ’s interface properties, but also the interface properties of its interface dependencies (which dene the public headers of boost and rapidjson in this case), and those of the dependencies of the dependencies, etc.
But how does CMake solve this problem? Easy, it adds all interface properties of Boost::boost and RapidJSON::RapidJSON to the corresponding JSONUtil ’s own interface properties. This means that users of JSONUtils will transitively receive the interface properties of targets all up the dependency chain.
On the other hand, Boost::regex is only used internally and can be a private dependency. Here, Boost::regexes interface properties will be appended to the corresponding JSONUtil ’s private
properties, and won’t be propagated to users.
Isn’t this beautiful? Usage requirements are propagated and build requirements encapsulated. Welcome to modern CMake.
关于痛苦的第三方依赖
自己来处理,70%的findmodule.cmake都没能够正确处理,所以你得亲自来做
CMake太灵活了,需要一个最佳实践!
Yes, welcome to hell. This is where the real pain begins: 3rdparty dependencies. In the case of rapidjson, a single variable is set to point to its include directories. This is exactly what we don’t want, we don’t want variables, we want targets!
In my case, 70% of my dependencies didn’t dene any targets in their nd modules or congs. The reality is that CMake usage is an anarchy. There are few rules and too much exibility. We need standard practices, we need guidelines. We have design patterns for C++, why not for CMake?
If you want it done right do it yourself
你需要考虑一下,有没有可以替换的组件支持modern cmake usage?
自己写!FindRapidJSON.cmake ourselves:
# FindRapidJSON.cmake
#
# Finds the rapidjson library #
# This will define the following variables #
#
#
RapidJSON_FOUND
RapidJSON_INCLUDE_DIRS
#
# and the following imported targets
#
# RapidJSON::RapidJSON
#
# Author: Pablo Arias - pabloariasal@gmail.com
find_package(PkgConfig)
pkg_check_modules(PC_RapidJSON QUIET RapidJSON)
find_path(RapidJSON_INCLUDE_DIR NAMES rapidjson.h
PATHS ${PC_RapidJSON_INCLUDE_DIRS} PATH_SUFFIXES rapidjson
)
set(RapidJSON_VERSION ${PC_RapidJSON_VERSION})
mark_as_advanced(RapidJSON_FOUND RapidJSON_INCLUDE_DIR RapidJSON_VERSION)
include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(RapidJSON REQUIRED_VARS RapidJSON_INCLUDE_DIR VERSION_VAR RapidJSON_VERSION
)
if(RapidJSON_FOUND)
set(RapidJSON_INCLUDE_DIRS ${RapidJSON_INCLUDE_DIR})
endif()
if(RapidJSON_FOUND AND NOT TARGET RapidJSON::RapidJSON) add_library(RapidJSON::RapidJSON INTERFACE IMPORTED) set_target_properties(RapidJSON::RapidJSON PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${RapidJSON_INCLUDE_DIR}"
)
endif()
This is a very simple nd module that looks for rapidjson’s headers in the system and creates the imported target RapidJSON::RapidJSON that we require. I use INTERFACE to indicate that this “library” isn’t really a library, as there is no correponding .a or .so, but just denes usage requirements.
并不产生a或者so文件,而是仅仅定义使用需求
外部如何使用我们的库呢?jsonutils
find_package(JSONUtils 1.0 REQUIRED)
target_link_libraries(example JSONUtils::JSONUtils)
为了用户能够正确使用我们的库,我们需要把库安装好
include(GNUInstallDirs)
install(TARGETS JSONUtils
EXPORT jsonutils-export
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
)
In CMake, installed targets are registered to exports using the EXPORT argument. Exports are therefore just a set of targets that can be exported and installed. Here we just told CMake to install our library and to register the target in the export jsonutils-export.
Then we can go ahead and install the export that we dened above:
install(EXPORT jsonutils-targets
FILE JSONUtilsTargets.cmake
NAMESPACE JSONUtils::
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/JSONUtils
)
This will install the import script JSONUtilsTargets.cmake that, when included in other scripts, will load the targets dened in the export jsonutils-export. By using the NAMESPACE argument, we tell CMake to prepend the prex JSONUtils:: to all targets imported.
自己写的config.cmake
Import your targets inside your Config.cmake
Remember, when clients call find_package(JSONUtils) , CMake will look for and execute a
JSONUtilsConfig.cmake .
So that our target JSONUtils::JSONUtils is imported and can be used by clients, we need to load JSONUtilsTargets.cmake in our config file
get_filename_component(JSONUtils_CMAKE_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH) include(CMakeFindDependencyMacro)
find_dependency(Boost 1.55 REQUIRED COMPONENTS regex) find_dependency(RapidJSON 1.0 REQUIRED MODULE)
if(NOT TARGET JSONUtils::JSONUtils)
include("${JSONUtils_CMAKE_DIR}/JSONUtilsTargets.cmake")
endif()
Be aware that JSONUtilsTargets.cmake contains code like:
add_library(JSONUtils::JSONUtils STATIC IMPORTED)
set_target_properties(JSONUtils::JSONUtils PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES "${_IMPORT_PREFIX}/include"
INTERFACE_LINK_LIBRARIES "Boost::boost;RapidJSON::RapidJSON;$<LINK_ONLY:Boost::regex>"
)
github (https://github.com/pabloariasal/modern-cmake-sample)