Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CMake简明教程 #5

Open
AlexiaChen opened this issue Oct 16, 2019 · 0 comments
Open

CMake简明教程 #5

AlexiaChen opened this issue Oct 16, 2019 · 0 comments
Labels
c/cpp C and CPP programming language 工具 使用,配置,学习笔记等

Comments

@AlexiaChen
Copy link
Owner

AlexiaChen commented Oct 16, 2019


title: CMake简明教程
date: 2018-08-12 17:39:00
tags:

  • C/C++
  • CMake

前言

主要最近的换工作,完全在Linux下开发,虽然以前都接触过CMake,不过体系也是零散的,遂做了一个简短的CMake教程,以供后续快速入门。

另外,好久也没有写文章了,这份工作还是有一定的技术性,之前的那家公司是开发/维护,大部分工作都是维护,没有什么写文章的激情。

所以,今天是硬凑一篇文章。

正文

CMake

CMake是跨平台的元构建系统,也就是说,它不实际产生构建行为,它只是生成给其他构建系统使用的文件,比如Makefile,MSBuild的solution file。

CMake根据读取名为CMakeLists.txt的文件,然后生成平台特定的构建文件,但是一个很大的问题是,CMake官方提供的教程特别复杂,对于新手的个坑,很难快速入门。

这个教程会通过例子来学习怎么用CMake。以下我们提供几个C++源代码供例子使用:

  • main.cpp
  • vector.h
  • vector.cpp
  • array.h
  • array.cpp

那么描述构建的CMakeLists.txt内容会是以下:

cmake_minimum_required(VERSION 2.8)
project(pjc-lab5)

set(CMAKE_CXX_FLAGS "-std=c++14 -Wall ${CMAKE_CXX_FLAGS}")

include_directories(${CMAKE_CURRENT_SOURCE_DIR})

add_executable(vector-test
    array.cpp
    vector.cpp
    main.cpp
)

以上代码很简单,但是第一个问题是,它是不可移植的,因为它没有任何逻辑判断就设置了GCC/Clang的特定编译参数。

第二个问题是,它全局改变了include的搜索路径。

CMake也要有好的书写习惯,采用更加现代的方式来写CMake文件:

cmake_minimum_required(VERSION 3.5)
project(pjc-lab5 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)


add_executable(vector-test
    array.cpp
    vector.cpp
    main.cpp
)

注意了,以上代码有几点改变了:

  • 强制要求了CMake的版本不得小于3.5,因为要使用CMake的一些新功能
  • 直接指定该工程为C++工程。这样可以减少CMake查找tool chain的时间,它就不会去查找其他编译器了,也不会检查其他编译器是否正常了。
  • 直接采用可跨平台的方式来指定采用的C++标准为C++ 14
  • 打开CMAKE_CXX_STANDARD_REQUIRED开关,如果C++ 14标准不被支持,CMake会直接终止构建过程。反之,会采用老的标准来构建
  • CMAKE_CXX_EXTENSIONS开关是告诉CMake采用更加通用的编译参数,比如这个开关打开,传递给GCC的参数就会是-std=c++14 而不是-std=gnu++14

然后在构建过程中,你会发现没有警告,因为CMake不会设定编译器的警告级别,需要你根据不同平台来指定相应的编译器警告参数:

if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU" )
    target_compile_options( vector-test PRIVATE -Wall -Wextra -Wunreachable-code -Wpedantic)
endif()
if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang" )
    target_compile_options( vector-test PRIVATE -Wweak-vtables -Wexit-time-destructors -Wglobal-constructors -Wmissing-noreturn )
endif()
if ( CMAKE_CXX_COMPILER_ID MATCHES "MSVC" )
    target_compile_options( vector-test PRIVATE /W4 /w44265 /w44061 /w44062 )
endif()

如果就采用上述的CMake文件,那么它生成的工程文件并不好,没有预期,你会发现如果生成VS的solution,你打开工程,你会发现没有包含头文件(vector.h array.h)。因为CMake不理解C++语言,它只是构建工具。

所以CMake文件中要改变下:

add_executable(vector-test
    array.cpp
    vector.cpp
    main.cpp
    array.h
    vector.h
)

当然,也可以通过CMake的source_group命令给文件归类:

source_group("Tests" FILES main.cpp)
source_group("Implementation" FILES array.cpp vector.cpp)

这样VS工程下就可以看到对C++源文件分类的文件夹图标了。

Tests

CMake是一堆工具的集合,所以它有一个test runner,叫CTest

要使用它,你需要显式指定它:

add_test(NAME test-name COMMAND how-to-run-it)

测试返回0表示成功,返回其他值表示失败。

还可以自定义,通过set_tests_properties来设置其相关属性

对于我们的例子工程,我们仅仅是运行bin文件,并不做额外检查:

include(CTest)
add_test(NAME plain-run COMMAND $<TARGET_FILE:vector-test>)

COMMAND后面的表达式是generator-expression

最后,我们的CMakeLists.txt的内容会是:

cmake_minimum_required(VERSION 3.5)
project(pjc-lab5 
DESCRIPTION "Very nice project"
LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)


add_executable(vector-test
    array.cpp
    vector.cpp
    main.cpp
    array.h
    vector.h
)

source_group("Tests" FILES main.cpp)
source_group("Implementation" FILES array.cpp vector.cpp)


if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang|AppleClang|GNU" )
    target_compile_options( vector-test PRIVATE -Wall -Wextra -Wunreachable-code -Wpedantic)
endif()
if ( CMAKE_CXX_COMPILER_ID MATCHES "Clang" )
    target_compile_options( vector-test PRIVATE -Wweak-vtables -Wexit-time-destructors -Wglobal-constructors -Wmissing-noreturn )
endif()
if ( CMAKE_CXX_COMPILER_ID MATCHES "MSVC" )
    target_compile_options( vector-test PRIVATE /W4 /w44265 /w44061 /w44062 )
endif()

include(CTest)
add_test(NAME plain-run COMMAND $<TARGET_FILE:vector-test>)

libraries

之前的教程都是很简单的例子,但是现实中的项目往往要拆分模块,链接外部的第三方库或者链接工程内的库。

如果不想阅读此章节,可以参考JetBrain的CLion提供的一个简明的CMake教程

里面记录如何包含链接外部的library。

构建生成一个library的cmake指令如下:

add_library(libname [STATIC | SHARED] two.cpp three.h)

下面我们来写一个计算器程序的cmake,bin文件依赖了lib文件,这样看起来就像一个小工程了:

cmake_minimum_required(VERSION 3.8)

project(Calculator LANGUAGES CXX)

add_library(calclib STATIC src/calclib.cpp include/calc/lib.hpp)
target_include_directories(calclib PUBLIC include)
target_compile_features(calclib PUBLIC cxx_std_11)

add_executable(calc apps/calc.cpp)
target_link_libraries(calc PUBLIC calclib)

变量和缓存变量

为什么要说这个?因为如果cmake维护一个很大的工程,会有各种编译策略参数,这个就需要逻辑判断,变量这些就随之诞生了。

给一个局部变量设置一个值:

set(MY_VARIABLE "value")

当然,cmake也有list类型的变量,有2种表达方式:

set(MY_LIST "one" "two")
set(MY_LIST "one;two")

以上变量如果离开作用域就无效了。

下面来说下缓存变量,如果你要从命令行来设置cmake的变量,那么就需要声明缓存变量。类似于CMAKE_BUILD_TYPE这样的变量都是缓存变量:

set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "Description")

但是以上不会替换已经存在的值,需要按照以下这样:

set(MY_CACHE_VARIABLE "VALUE" CACHE STRING "" FORCE)
mark_as_advanced(MY_CACHE_VARIABLE)

环境变量

cmake可以访问环境变量:

set(ENV{variable_name} value)
set(MY_VAR $ENV{variable_name})

属性

cmake也会在属性中保存一些信息,这些属性有点像变量,但是这些属性的作用一般是针对一个target或者目录什么的。cmake的属性变量是以CMAKE_打头的,类似CMAKE_BUILD_TYPE,CMAKE_CXX_STANDARD这些。

比如使用CXX标准可以用设置属性的方式办到,有两种表达:

set_property(TARGET TargetName
             PROPERTY CXX_STANDARD 11)

set_target_properties(TargetName PROPERTIES
                      CXX_STANDARD 11)

当然既然有set,当然有get:

get_property(ResultVariable TARGET TargetName PROPERTY CXX_STANDARD)

Cmake编程

控制流

if(variable)
    # If variable is `ON`, `YES`, `TRUE`, `Y`, or non zero number
else()
    # If variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-NOTFOUND`
endif()
if("${variable}")
    # True if variable is not false-like
else()
    # Note that undefined variables would be `""` thus false
endif()

宏和函数

function(SIMPLE REQUIRED_ARG)
    message(STATUS "Simple arguments: ${REQUIRED_ARG}, followed by ${ARGV}")
    set(${REQUIRED_ARG} "From SIMPLE" PARENT_SCOPE)
endfunction()

simple(This)
message("Output: ${This}")

与源码文件“通信”

Cmake允许源代码访问cmake的变量,这个指令就是configure_file。

这样的功能在版本管理上经常使用:

Version.h.in

#pragma once

#define MY_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MY_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MY_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MY_VERSION_TWEAK @PROJECT_VERSION_TWEAK@
#define MY_VERSION "@PROJECT_VERSION@"
configure_file (
    "${PROJECT_SOURCE_DIR}/include/My/Version.h.in"
    "${PROJECT_BINARY_DIR}/include/My/Version.h"
)

可以从上面的配置看出来,本质上cmake只是把后缀in的文件,进行变量标记替换,然后再拷贝到指定目录的文件名。没有什么神奇的。

当然,cmake也可以反过来,cmake代码访问源码文件的内容:

# Assuming the canonical version is listed in a single line
# This would be in several parts if picking up from MAJOR, MINOR, etc.
set(VERSION_REGEX "#define MY_VERSION[ \t]+\"(.+)\"")

# Read in the line containing the version
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/include/My/Version.hpp"
    VERSION_STRING REGEX ${VERSION_REGEX})

# Pick out just the version
string(REGEX REPLACE ${VERSION_REGEX} "\\1" VERSION_STRING "${VERSION_STRING}")

# Automatically getting PROJECT_VERSION_MAJOR, My_VERSION_MAJOR, etc.
project(My LANGUAGES CXX VERSION ${VERSION_STRING})

怎样规划你的工程结构

基本如下:

- project
  - .gitignore
  - README.md
  - LICENCE.md
  - CMakeLists.txt
  - cmake
    - FindSomeLib.cmake
  - include
    - project
      - lib.hpp
  - src
    - CMakeLists.txt
    - lib.cpp
  - apps
    - CMakeLists.txt
    - app.cpp
  - tests
    - testlib.cpp
  - docs
    - Doxyfile.in
  - extern
    - googletest
  - scripts
    - helper.py

运行其他的程序

编译的时候要做很多事情,比如你构建完毕要用nsis进行打包等等,有些时候不得不调用其他命令行。

配置时运行一个命令

find_package(Git QUIET)

if(GIT_FOUND AND EXISTS "${PROJECT_SOURCE_DIR}/.git")
    execute_process(COMMAND ${GIT_EXECUTABLE} submodule update --init --recursive
                    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
                    RESULT_VARIABLE GIT_SUBMOD_RESULT)
    if(NOT GIT_SUBMOD_RESULT EQUAL "0")
        message(FATAL_ERROR "git submodule update --init failed with ${GIT_SUBMOD_RESULT}, please checkout submodules")
    endif()
endif()

编译时运行一个命令

find_package(PythonInterp REQUIRED)
add_custom_command(OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp"
    COMMAND "${PYTHON_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/GenerateHeader.py" --argument
    DEPENDS some_target)

add_custom_target(generate_header ALL
    DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp")

install(FILES ${CMAKE_CURRENT_BINARY_DIR}/include/Generated.hpp DESTINATION include)

Cmake其他的一些特性

CMAKE_BUILD_TYPE变量默认既不是Debug也不是Release。必须指定。

关于C++ 11等一些新标准,有以下配置:

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

set_target_properties(myTarget PROPERTIES
    CXX_STANDARD 11
    CXX_STANDARD_REQUIRED YES
    CXX_EXTENSIONS NO
)

查找库

find_library(MATH_LIBRARY m)
if(MATH_LIBRARY)
    target_link_libraries(MyTarget PUBLIC ${MATH_LIBRARY})
endif()
@AlexiaChen AlexiaChen added c/cpp C and CPP programming language 工具 使用,配置,学习笔记等 labels Oct 16, 2019
@AlexiaChen AlexiaChen reopened this Aug 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c/cpp C and CPP programming language 工具 使用,配置,学习笔记等
Projects
None yet
Development

No branches or pull requests

1 participant