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 ReleaseDockerfile编译安装
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_APIcmake中如何给特定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文件访问和使用。