CMake

CMakeLists

模板

cmake_minimum_required(VERSION 3.16)
project(YourProject)

set(SOURCE_DIR "${CMAKE_SOURCE_DIR}/src")

include_directories(${CMAKE_SOURCE_DIR}/include)
link_directories(${CMAKE_SOURCE_DIR}/libs)

add_executable(main main.cpp)
target_link_libraries(main lib0 lib1)

install(TARGETS main DESTINATION ./out)
install(FILES ${PATH_TO_YOUR_FILE} DESTINATION ./out)

常见操作

开关

options(BUILD_WITH_TOOLS "build with tools or not" OFF)

判断条件

# 判断路径是否是文件夹
if (EXISTS "${YOUR_DIR_VAR}" AND IS_DIRECTORY "${YOUR_DIR_VAR}")
...
endif()

# 判断字符串变量是否为空
if (YOUR_STR_VAR STREQUAL "some string")
...
endif()

# 正则表达式匹配字符串变量
if(YOUR_STR_VAR MATCHES "reg expr")
...
endif()

string操作

replace

string(REPLACE <match-string> <replace-string> <out-var> <input>...)

正则匹配

string(REGEX MATCH ".*opencv_world.*d\\.dll" match_result "${dll}")
if(match_result)
	message("Matched")
else()
	message("Not matched")
endif()

文件查询

# 查询所有匹配的文件
FILE(GLOB_RECURSE SOURCE ./src/*.cpp)

# 删除特定的
list(REMOVE_ITEM SOURCE "${CMAKE_CURRENT_LIST_DIR}/src/something.cpp")

编译时生成文件

编译时动态生成文件,可以用来在CMakeLists中给项目传递数据,比如版本号、宏定义、或者是字符串(比如拷贝cl kernel,opencl shader到头文件中什么的)。

举个例子,在opencl项目中,通常要考虑如何放置内核代码,一种常见的方式是写到.cl文件中,执行的时候加载,但这样会导致发布的时候需要将.cl文件一起发布出去。另外可以考虑将.cl的代码直接写到头文件中,但是这样需要保持.cl文件中的代码和头文件中的代码一致(直接在头文件的string里写代码的狠人当我没说),这里就可以利用这个特性,在项目生成的时候,生成头文件,将.cl文件包进去。

首先是模板文件kernel_source.h.in的内容:

// kernel_source.h.in 的内容
#ifndef KERNEL_SOURCES_H_
#define KERNEL_SOURCES_H_

#include <string>
#include <unordered_map>
#include "kernel_source_defs.h" // 其他的一些和cl文件无关的定义

// ATTENTION: This file is generate from kernel_sources.h.in by cmake,
//            If you want to change somethings, pls change kernel_sources.h.in

const std::unordered_map<std::string, std::string> ProgramSources = {
    {PROG_NAME_WARPAFFINE, R"(@CLOPS_WARPAFFINE_SOURCE_CODE@)"},
    {PROG_NAME_RESIZE, R"(@CLOPS_RESIZE_SOURCE_CODE@)"},
    {PROG_NAME_CONVERT, R"(@CLOPS_CONVERT_SOURCE_CODE@)"},
    {PROG_NAME_MATH, R"(@CLOPS_MATH_SOURCE_CODE@)"},
    {PROG_NAME_FACE_PARSING, R"(@CLOPS_FACE_PARSING_SOURCE_CODE@)"}};


#endif  // KERNEL_SOURCES_H_

kernel_source_defs.h的内容,虽然对当前讨论的问题没什么影响,但为了demo完整起见,还是贴出来了:

#ifndef KERNEL_SOURCE_DEFS_H_
#define KERNEL_SOURCE_DEFS_H_

#include <string>
#include <unordered_map>

const char PROG_NAME_WARPAFFINE[] = "warpaffine";
const char PROG_NAME_RESIZE[] = "resize";
const char PROG_NAME_CONVERT[] = "convert";
const char PROG_NAME_MATH[] = "math";
const char PROG_NAME_FACE_PARSING[] = "face_parsing";

const std::unordered_map<std::string, std::string> kernelNameToProgramNameMap =
    {{"warpAffine_nearest_8uc1", PROG_NAME_WARPAFFINE},
     {"warpAffine_nearest_8uc3", PROG_NAME_WARPAFFINE},
     {"warpAffine_nearest_8uc4", PROG_NAME_WARPAFFINE},
     {"warpAffine_linear_8uc1", PROG_NAME_WARPAFFINE},
     {"warpAffine_linear_32fTo32fc1", PROG_NAME_WARPAFFINE},
     {"warpAffine_linear_8uc3", PROG_NAME_WARPAFFINE},
     {"warpAffine_linear_8uTo32fc3", PROG_NAME_WARPAFFINE},
     {"warpAffine_linear_8uc4", PROG_NAME_WARPAFFINE},
     // resize
     {"resize_nearest_8uc1", PROG_NAME_RESIZE},
     {"resize_nearest_8uc3", PROG_NAME_RESIZE},
     {"resize_nearest_8uc4", PROG_NAME_RESIZE},
     {"resize_linear_8uc1", PROG_NAME_RESIZE},
     {"resize_linear_8uc3", PROG_NAME_RESIZE},
     {"resize_linear_8uc4", PROG_NAME_RESIZE},
     // convert
     {"convert_8uTo8u_elm4", PROG_NAME_CONVERT},
     {"convert_32fTo32f_elm4", PROG_NAME_CONVERT},
     {"convert_8uTo32f_elm4", PROG_NAME_CONVERT},
     {"convert_32fTo8u_elm4", PROG_NAME_CONVERT},
     // math
     {"mad_8uTo32f_elm4", PROG_NAME_MATH},
     // face_parsing
     {"face_parsing_postprocess_c15", PROG_NAME_FACE_PARSING}};

#endif  // KERNEL_SOURCE_DEFS_H_

然后是CMakeLists中的内容:

  # Put the clops's cl kernel into kernel_source.h
  file(READ ${PROJECT_SOURCE_DIR}/cl/kernels/warpaffine.cl CLOPS_WARPAFFINE_SOURCE_CODE)
  file(READ ${PROJECT_SOURCE_DIR}/cl/kernels/resize.cl CLOPS_RESIZE_SOURCE_CODE)
  file(READ ${PROJECT_SOURCE_DIR}/cl/kernels/convert.cl CLOPS_CONVERT_SOURCE_CODE)
  file(READ ${PROJECT_SOURCE_DIR}/cl/kernels/math.cl CLOPS_MATH_SOURCE_CODE)
  file(READ ${PROJECT_SOURCE_DIR}/cl/kernels/face_parsing.cl CLOPS_FACE_PARSING_SOURCE_CODE)
  configure_file(
    ${PROJECT_SOURCE_DIR}/kernel_sources.h.in
    ${PROJECT_SOURCE_DIR}/kernel_sources.h
  )

这样项目生成的时候就会自动生成kernel_sources.h文件,并且将cl代码直接拷进去了。

编译后执行命令

主要是通过POST_BUILD指令,当TARGET conv3x3_generator构建完成之后,会执行COMMAND命令。VERBATIM表示会按照字面文本执行,而不会进行转义。

add_custom_command(
        TARGET conv3x3_generator
        POST_BUILD
        COMMAND ${CMAKE_CURRENT_BINARY_DIR}/conv3x3_generator -g conv3x3_halide -e ${HALIDE_EMIT_OPTIONS} -o ${CMAKE_CURRENT_BINARY_DIR} target=${HALIDE_TARGET}
        COMMENT "use halide generator to generate the need hexagon implementation."
        VERBATIM
    )

添加中间生成文件的依赖

cmake_minimum_required(VERSION 3.14.1)

project(lessons CXX)

set(HALIDE_SDK_ROOT /home/faceunity/Library/Halide)

set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} ${HALIDE_SDK_ROOT})

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)

find_package(Halide REQUIRED)

# First compile test_generator.
add_executable(test_generator
  src/test_generator.cpp
  ${HALIDE_SDK_ROOT}/share/Halide/tools/GenGen.cpp)
target_link_libraries(test_generator PRIVATE Halide::Halide Halide::Tools)
set(HALIDE_EMIT_OPTIONS o,h)
set(HALIDE_TARGET x86-64-linux-avx2)

# 添加第一层依赖,test_halide.o的生成依赖test_generator
add_custom_command(
  OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/test_halide.o
  COMMAND ${CMAKE_CURRENT_BINARY_DIR}/test_generator -g test_halide -e ${HALIDE_EMIT_OPTIONS} -o ${CMAKE_CURRENT_BINARY_DIR} target=${HALIDE_TARGET}
  DEPENDS test_generator
  VERBATIM
)
# 将文件添加为custom_target
add_custom_target(test_halide ALL
  DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/test_halide.o
)

# test的生成依赖test_halide.o对应的custom_target.
add_executable(test src/test.cpp)
add_dependencies(test test_halide)
target_include_directories(test PRIVATE ${CMAKE_CURRENT_BINARY_DIR})
target_link_libraries(test PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/test_halide.o)
target_link_libraries(test PRIVATE dl pthread)

添加find_package

# used to find fugan libs

# Runtime install path got.
get_filename_component(CMAKE_CURRENT_LIST_DIR "${CMAKE_CURRENT_LIST_FILE}" PATH)
# Extract the directory where *this* file has been installed (determined at cmake run-time)
# Get the absolute path with no ../.. relative marks, to eliminate implicit linker warnings
get_filename_component(FUGAN_CONFIG_PATH "${CMAKE_CURRENT_LIST_DIR}" REALPATH)
# release/libs/cmake/fuganConfig.cmake
get_filename_component(FUGAN_INSTALL_PATH "${FUGAN_CONFIG_PATH}/../../" REALPATH)

set(FUGAN_INCLUDE_DIR ${FUGAN_INSTALL_PATH}/include)
add_library(fugan SHARED IMPORTED)
add_library(fugan_wav SHARED IMPORTED) # For tools
add_library(fugan_trtapp SHARED IMPORTED) # For runtimepack tools
if (MSVC)
  set(FUGAN_LIBS_DIR ${FUGAN_INSTALL_PATH}/libs/win64)
  # On Windows, the library is a .dll, but the corresponding import library is usually a .lib
  # You may need to specify the import library if you're linking against it in MSVC
  set_target_properties(fugan PROPERTIES
      INTERFACE_INCLUDE_DIRECTORIES "${FUGAN_INCLUDE_DIR}"
      IMPORTED_LOCATION "${FUGAN_LIBS_DIR}/fugan.dll"
      IMPORTED_IMPLIB "${FUGAN_LIBS_DIR}/fugan.lib"  # Only needed if you use the .lib import library
  )
  set_target_properties(fugan_wav PROPERTIES
      INTERFACE_INCLUDE_DIRECTORIES "${FUGAN_INCLUDE_DIR}"
      IMPORTED_LOCATION "${FUGAN_LIBS_DIR}/wav.dll"
      IMPORTED_IMPLIB "${FUGAN_LIBS_DIR}/wav.lib")
  
  add_library(fugan_soloud STATIC IMPORTED)
  set_target_properties(fugan_soloud PROPERTIES
      INTERFACE_INCLUDE_DIRECTORIES "${FUGAN_INCLUDE_DIR}"
      IMPORTED_LOCATION ${FUGAN_LIBS_DIR}/soloud.lib)

  set_target_properties(fugan_trtapp PROPERTIES
      INTERFACE_INCLUDE_DIRECTORIES "${FUGAN_INCLUDE_DIR}"
      IMPORTED_LOCATION "${FUGAN_LIBS_DIR}/trtapp.dll"
      IMPORTED_IMPLIB "${FUGAN_LIBS_DIR}/trtapp.lib")
elseif(UNIX)
  set(FUGAN_LIBS_DIR ${FUGAN_INSTALL_PATH}/libs/linux_x86_64)
  # On Linux, it's likely to be a .so file, on macOS a .dylib
  set_target_properties(fugan PROPERTIES
    INTERFACE_INCLUDE_DIRECTORIES "${FUGAN_INCLUDE_DIR}"
    IMPORTED_LOCATION "${FUGAN_LIBS_DIR}/libfugan.so"
  )
  set_target_properties(fugan_wav PROPERTIES
      INTERFACE_INCLUDE_DIRECTORIES "${FUGAN_INCLUDE_DIR}"
      IMPORTED_LOCATION "${FUGAN_LIBS_DIR}/libwav.so")
  set_target_properties(fugan_trtapp PROPERTIES
      INTERFACE_INCLUDE_DIRECTORIES "${FUGAN_INCLUDE_DIR}"
      IMPORTED_LOCATION "${FUGAN_LIBS_DIR}/libtrtapp.so")
endif()

添加target别名

# 已经存在一个target
add_library(alias_target ALIAS target)

常见变量和选项

CMAKE_SHARED_LINKER_FLAGS

它包含了构建共享库时传递给链接器的额外标志。

# -Wl,--exclude-libs,ALL它告诉链接器在链接共享库时排除所有的标准系统库
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -Wl,--exclude-libs,ALL")

CMAKE_SYSTEM_NAME

标识操作系统的名称,他在跨平台项目编译上很有用,通常有以下数值:

  • Linux: Linux系统。
  • Windows: Windows操作系统。
  • Darwin: macOS系统。
  • FreeBSD: FreeBSD系统。

CMAKE_PREFIX_PATH

定义了其他包install的位置,需要是绝对路径,里面的结构一般有:

install
├── include
   └── CL
       ├── cl2.hpp
       └── opencl.hpp
└── share
    ├── cmake
       └── OpenCLHeadersCpp
           ├── OpenCLHeadersCppConfig.cmake
           ├── OpenCLHeadersCppConfigVersion.cmake
           └── OpenCLHeadersCppTargets.cmake
    └── pkgconfig
        └── OpenCL-CLHPP.pc

其中share文件夹中包含的内容用于其他库来寻找当前库。

-Os

-Os是针对大小的优化选项。

平台相关

平台的判断

# 或者直接if (MSVC) 判断为windows平台
if (MSVC_WIN64) 
	# win64
elseif (MSVC_WIN32)
	# win32
elseif (IOS)
	# IOS
elseif (APPLE)
	# macos
	if (CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
		# macos arm64
	elseif (CMAKE_OSX_ARCHITECTURES MATCHES "x86_64")
		# macos x86_64
	endif()
elseif (ANDROID)
	# set path use ${CMAKE_ANDROID_ARCH_ABI}
elseif (UNIX)
	# Linux
endif()

编译

cmake常用命令

# 在当前文件夹创建build文件夹,并且生成项目
# -S:指定源代码文件夹,也就是包含CMakeLists.txt的文件夹
# -B:指定build文件夹,也就是项目生成目录
cmake -S . -B build -DCMAKE_INSTALL_PREFIX=/chosen/install/prefix -DCMAKE_PREFIX_PATH=/path/to/your/lib/install

# 在当前文件夹编译build文件夹中的项目,并且执行install
cmake --build build --target install

# 常规操作。
mkdir build
cd build
cmake ..

# 然后是build
cmake --build .

# 如果要加特定的target的话
cmake --build . --target your_target

# 如果要执行install的话
cmake --install .

# 对于windows如何要设置Release/Debug需要
cmake --build %BUILD_DIR% --config Release

Dockerfile编译安装

RUN apt install -y --no-install-recommends libssl-dev wget build-essential cmake pkg-config
# Install cmake 3.16
RUN wget -P /tmp https://github.com/Kitware/CMake/archive/refs/tags/v3.16.1.tar.gz && \
    cd /tmp && mkdir cmake && tar -xf ./v3.16.1.tar.gz -C ./cmake --strip-components=1 && \
    cd ./cmake && ./configure --prefix=/usr/local && make -j16 && make install && \
    cd /tmp && rm -rf cmake && rm -rf ./v3.16.1.tar.gz

常见问题

子项目依赖实践

示例项目地址:kaihang/OpenCLUtils.git

如果当前项目依赖了子项目,如何优雅的实现依赖编译:

// 项目目录结构
.
├── 3rdparty
   ├── CMakeLists.txt
   ├── OpenCL-CLHPP
   ├── OpenCL-Headers
   ├── OpenCL-ICD-Loader
   └── spdlog
├── build_android.sh
├── CMakeLists.txt
├── examples
   └── image_warpaffine_test.cpp
├── src
   └── imgproc
└── test.sh

其中3rdparty中的都是submodulegit submodule add your_repo_id ./3rdparty/yourdir

3rdparty/CMakeLists.txt中的内容如下:

# 需要注意顺序,后者依赖的库的需要放前面,eg:OpenCL-ICD-Loader依赖OpenCL-Headers,而OpenCL-CLHPP依赖OpenCL-ICD-Loader和OpenCL-Headers。
# spdlog
add_subdirectory(spdlog)
# OpenCL::Headers
add_subdirectory(OpenCL-Headers)
# OpenCL::OpenCL
add_subdirectory(OpenCL-ICD-Loader)
# OpenCL::HeadersCpp
add_subdirectory(OpenCL-CLHPP)

CMakeLists.txt中的内容如下:

cmake_minimum_required(VERSION 3.10)
project(opencl_utils)

set(SOURCE_DIR ${CMAKE_SOURCE_DIR}/src)
set(EXAMPLES_DIR ${CMAKE_SOURCE_DIR}/examples)

FILE(GLOB_RECURSE HeaderFiles ${SOURCE_DIR}/*.hpp)
FILE(GLOB_RECURSE SourceFiles ${SOURCE_DIR}/*.cpp)

include_directories(${SOURCE_DIR})
link_directories(${OPENCL_LIB_DIR})

add_subdirectory(3rdparty)

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Os")

# add_library(ocl_imgproc STATIC ${SourceFiles})
# target_link_libraries(ocl_imgproc )

add_executable(image_warpaffine_test ${EXAMPLES_DIR}/image_warpaffine_test.cpp)
target_link_libraries(image_warpaffine_test)

# install(TARGETS ocl_imgproc DESTINATION ${CMAKE_SOURCE_DIR}/out)
install(TARGETS image_warpaffine_test DESTINATION ${CMAKE_SOURCE_DIR}/out)

build_android.sh的内容:

export ANDROID_NDK_HOME=/home/faceunity/Programs/Android/NDK/android-ndk-r26b
export ANDROID_ABI="arm64-v8a"
export ANDROID_API=21

cmake -S . -B build_android \
        -DCMAKE_INSTALL_PREFIX=./out \
        -G "Unix Makefiles" \
        -DANDROID_STL=c++_static \
        -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
        -DANDROID_NDK=$ANDROID_NDK_HOME \
        -DANDROID_TOOLCHAIN=clang \
        -DANDROID_ABI=$ANDROID_ABI \
        -DANDROID_NATIVE_API_LEVEL=$ANDROID_API

使用Android toolchain进行编译

export ANDROID_SDK=/home/faceunity/Programs/Android/SDK
export ANDROID_HOME=$ANDROID_SDK
export ANDROID_NDK_HOME=/home/faceunity/Programs/Android/NDK/android-ndk-r26b
export ANDROID_ABI="arm64-v8a"
export ANDROID_API=21

mkdir -p build
cd build

cmake .. \
        -G "Unix Makefiles" \
        -DANDROID_STL=c++_static \
        -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK_HOME/build/cmake/android.toolchain.cmake \
        -DANDROID_SDK=$ANDROID_SDK \
        -DANDROID_NDK=$ANDROID_NDK_HOME \
        -DANDROID_HOME=$ANDROID_SDK \
        -DANDROID_TOOLCHAIN=clang \
        -DANDROID_ABI=$ANDROID_ABI \
        -DANDROID_NATIVE_API_LEVEL=$ANDROID_API

cmake中如何给特定target设置-fPIC

-fPIC(Position Independent Code,PIC)是为了生成位置无关的代码的编译选项,位置无关代码是一种特殊的机器码,可以在内存中任何位置执行,通常情况下是用于创建动态库的。

target_compile_options(your_target_name PRIVATE -fPIC)

如何在编译完之后执行特定给定指令,比如strip

add_library(your_target SHARED ${SOURCES})
add_custom_command(TARGET your_target POST_BUILD
  COMMAND llvm-strip "libyour_target.so"
  COMMENT "Stripping libyour_target.so"
)

编译静态库的时候,中间target需要设置成OBJECT而不是STATIC

# 需要使用OBJECT,如果使用STATIC的话,会输出libpasd.a和libpasd_interface.a两个库,导致对外的话需要导出两个库。
add_target(pasd OBJECT ${SOURCES})
add_target(pasd_interface STATIC pasd)

动态链接的时候如果stdc++或者别的库有冲突的话,会失败

考虑静态链接。

libc++

默认链接动态的libc++_shared.so, 可以手动改成libc++_static.a

add_subdirectory变量作用域问题

subdirectory中定义的变量,是不会影响上层的,包括include_directories等。可以通过include而不是add_subdirectory来干这件事。

子项目中如果有set(SOME_PATH "default_path" CACHE PATH "doc string")需要在上层项目中通过CMakeLists.txt修改的话,需要在add_subdirectory之前定义该变量,并且不能直接set(SOME_PATH "your_path")这样定义,初次生成项目的时候会被后续带有CACHE PATH的声明覆盖掉,正确的方法是set(SOME_PATH "your_path" CACHE PATH "doc string" FORCE)

子项目静态库/动态库包含三方库连接相关问题

如果创建的是静态库,该静态库不会将其链接的三方库包进去,外部链接该静态库需要把三方库也链接进去。

如果创建的是动态库,则该动态库会直接把三方静态库都包进去。

子项目的三方库或者include,如果需要别的项目进行依赖需要加上PUBLIC

target_link_libraries(target PUBLIC 3rdlib)
target_include_directories(target PUBLIC include)

set的CACHE关键字

CACHE关键字指定可缓存的变量,例如set(MY_VAR "Test" CACHE STRING "Doc of the variable."),有以下几个特点

  • 缓存机制:cmake配置运行的时候,CACHE变量会被存储到CMakeCache.txt中,这意味着下一次运行CMake时,如果没有显示的指定该变量的值-DMY_VAR="Something",将会使用缓存的值,而不是再次使用默认值
  • 用户可配置:可以在cmake-gui中进行配置。
  • 可访问性:通过缓存机制,可以在不同的CMakeLists.txt文件之间共享变量的值。例如,主CMakeLists.txt文件定义了一个缓存变量,这个变量可以被子项目的CMakeLists.txt文件访问和使用。