如何為cmake提供package以便于find_package, 以及用VCPKG補(bǔ)充CMake實現(xiàn)快速下載集成

1. CMake帶來的改變

1.1 依賴關(guān)系的思維轉(zhuǎn)變:用倉庫的概念代替目錄層級依賴

層級依賴:

├── TaihuApp
│       └── Qt::Quick
│       └── Qt5::Core
│       └── Qt5::Widgets
│       └── opencv ─────────────────────────────────┐ 
│       └── logger ─────────────────┐           |
│       └── gtest ────────────────┐ [duplicate]     |
│       └── camera            |     |       [duplicate]
│               └── opencv     ───|─────|───────────┘   
│               └── baumer        |     |           
│               └── tucsen  [duplicate] |           
│               └── protocol      |     |           
│               └── logger ───|─────┘   
│               └── gtest ────────┘

扁平依賴:

├── Repository ────>──>──>──────│ 
│       └── Qt::Quick           |
│       └── Qt5::Core           |
│       └── Qt5::Widgets        |────────── First Project
│       └── camera              |
│       └── logger              |
│       └── gtest               |────────── Second Project
│       └── opencv              |
│       └── baumer              |
│       └── tucsen              |────────── Other project
│       └── protocol            |
│       └── any other libs      |

圖一:在每個項目里都存放一套自身需要的依賴庫,類似離線式依賴包含關(guān)系;
圖二:camera依賴了opencv、baumer等別的庫,但不存在包含關(guān)系,倉庫里所有庫的依賴關(guān)系都是通過配置進(jìn)行關(guān)聯(lián)的,本質(zhì)所有的庫都在項目之外的倉庫里存放的。

1.2 簡潔優(yōu)雅的庫依賴集成方式

project(camera VERSION 1.0.0)

find_package(protocol REQUIRED)
find_package(logger REQUIRED)
find_package(timer REQUIRED)
find_package(opencv REQUIRED)
find_package(baumer REQUIRED)
find_package(tucsen REQUIRED)

aux_source_directory(. SRC_LIST)
add_library(${PROJECT_NAME} STATIC ${SRC_LIST})

target_link_libraries(${PROJECT_NAME} PRIVATE
    protocol
    smt-logger
    smt-timer
    baumer
    tucsen
    opencv)

target_include_directories(${PROJECT_NAME} PUBLIC
    $<BUILD_INTERFACE:${CMAKE_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>)

建議用target_include_directory()代替include_directory(),如果當(dāng)前也是一個對外提供api的庫

1.3 依賴庫版本控制(vcpkg賦能)

{
  "name": "project",
  "version-string": "1.0.0",
  "supports": "(x64 | arm64) & (linux | osx | windows)",
  "dependencies": [
    { "name": "zlib", "version>=": "1.2.11#9" },
    { "name": "fmt", "version>=": "7.1.3#1" }
  ]
}

允許指定當(dāng)前庫的對外名字、版本、適用于哪些平臺系統(tǒng)、以及依賴哪些別的庫甚至那些庫的指定版本

2. 自己的庫如何能被find_pakcage(xxx)

cmake有兩種方式讓find_package(xxx) 能找到庫,如果沒有找到會報錯,如下:

find_package(OpenCV)出現(xiàn)錯誤如下:

CMake Warning at CMakeLists.txt:37 (find_package):
  By not providing "FindOpenCV.cmake" in CMAKE_MODULE_PATH this project has
  asked CMake to find a package configuration file provided by "OpenCV", but
  CMake did not find one.

  Could not find a package configuration file provided by "OpenCV" with any of
  the following names:

    OpenCVConfig.cmake
    OpenCV-config.cmake

  Add the installation prefix of "OpenCV" to CMAKE_PREFIX_PATH or set "OpenCV_DIR"
  to a directory containing one of the above files.  If "OpenCV" provides a
  separate development package or SDK, be sure it has been installed.

簡單翻譯下:

??cmake優(yōu)先會以Moudule模式尋找,即:搜索CMAKE_MODULE_PATH指定路徑下的FindXXX.cmake文件,默認(rèn)路徑按系統(tǒng)平臺區(qū)分如下:

  • windows: C:/Program Files/CMake/share/cmake-3.xx/Modules
  • linux: /usr/share/cmake-3.xx/Modules

??一旦找到了FindXXX.cmake, 則此庫一般會提供以下變量,目的是方便調(diào)用者快速集成它:

<NAME>_FOUND
<NAME>_INCLUDE_DIRS or <NAME>_INCLUDES 
<NAME>_LIBRARIES or <NAME>_LIBS

??如果沒能找到FindXXX.cmake, 則嘗試以Config模式:搜索指定路徑下的XXXConfig.cmake或者XXX-config.cmake文件,搜索路徑優(yōu)先是cmake install的路徑:

  • windows: C:/Program Files
  • linux: /usr/local

當(dāng)然也支持在項目里通過CMAKE_PREFIX_PATH指定了尋找路徑,或者直接通過設(shè)置XXX_DIR告知準(zhǔn)確的查找路徑。其實,還有一種做法是通過指定toolchain讓cmake統(tǒng)一從toolchain里尋找。

2.1 Config方式

??這是一種基于有項目源碼的方式,需要為cmake組織的項目提供完整的install腳本,當(dāng)執(zhí)行install時候會在install目的地的lib目錄下創(chuàng)建share目錄,并在share目錄里自動生成XXXConfig.cmake或者xxx-config.cmake等配置文件

??cmake install的腳本相對比較通用,已經(jīng)被我整理并抽取出來了,一般只要加在cmake項目的實現(xiàn)模塊的CMakeList.txt最下面即可,如下:

# ============================== install script ==============================
set(HEADERS ${CMAKE_SOURCE_DIR}/include/swc_camera.h)
set_target_properties(${PROJECT_NAME} PROPERTIES PUBLIC_HEADER "${HEADERS}")

# Install the target and create export-set
install(TARGETS ${PROJECT_NAME}
    EXPORT ${PROJECT_NAME}Targets
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
    PUBLIC_HEADER DESTINATION include)

# Generate the version file for the config file
include(CMakePackageConfigHelpers)
write_basic_package_version_file(
    ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    VERSION ${PACKAGE_VERSION}
    COMPATIBILITY SameMajorVersion)

# Exporting Targets from the Build Tree
install(EXPORT ${PROJECT_NAME}Targets
    DESTINATION "lib/cmake/${PROJECT_NAME}")

# Create config file
configure_package_config_file(
    ${CMAKE_SOURCE_DIR}/Config.cmake.in ${PROJECT_NAME}Config.cmake
    INSTALL_DESTINATION "lib/cmake/${PROJECT_NAME}")

# Install config files
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}Config.cmake
    ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}ConfigVersion.cmake
    DESTINATION "lib/cmake/${PROJECT_NAME}")

可以通過設(shè)置 CMAKE_INSTALL_PATH指定庫安裝的位置,cmake install library的命令是cmake --build ./ --target install, 在linux下配合make可以簡化為make install,這是makefile支持的:

cmake --build ./ --target install執(zhí)行后如下:

PS E:\swc-camera\build> cmake --build ./ --target install
Microsoft (R) Build Engine version 16.11.1+3e40a09f8 for .NET Framework
Copyright (C) Microsoft Corporation. All rights reserved.

  swc-camera.vcxproj -> E:\swc-camera\build\src\Debug\swc-camera.lib
  example.vcxproj -> E:\swc-camera\build\example\Debug\example.exe
  tests.vcxproj -> E:\swc-camera\build\tests\Debug\tests.exe
  -- Install configuration: "Debug"
  -- Up-to-date: E:/LOCAL_REPOSITORY/lib/swc-camera.lib
  -- Up-to-date: E:/LOCAL_REPOSITORY/include/swc_camera.h
  -- Up-to-date: E:/LOCAL_REPOSITORY/lib/cmake/swc-camera/swc-cameraTargets.cmake      
  -- Up-to-date: E:/LOCAL_REPOSITORY/lib/cmake/swc-camera/swc-cameraTargets-debug.cmake
  -- Up-to-date: E:/LOCAL_REPOSITORY/lib/cmake/swc-camera/swc-cameraConfig.cmake       
  -- Up-to-date: E:/LOCAL_REPOSITORY/lib/cmake/swc-camera/swc-cameraConfigVersion.cmake

2.2 Module方式

??這是一種當(dāng)?shù)谌綆靸H僅提供了編譯好的binary庫時候, 有時候有些庫編譯過程非常復(fù)雜且依賴多而且非常耗時,我們也可以用這種方式,為了讓find_package(xxx)找到它的方式。我們需要寫一個對應(yīng)的FindXXX.cmake,在FindXXX.cmake里會指定嘗試尋找?guī)焖诘穆窂?,一般非常主流的庫cmake的modules目錄會提供,但以下三種情況需要自己編寫FindXXX.cmake

  • cmake的modules目錄里提供的FindXXX.cmake描述的版本號和要用的不一致
  • 非大眾庫,如baumer或者tucsen,cmake是不可能提供FindXXX.cmake

在linux/mac系統(tǒng)里,大眾庫的FindXXX.cmake一般存在/usr/share/cmake-3.xx/Modules
在windows系統(tǒng)里,大眾庫的FindXXX.cmake存在C:\Program Files\CMake\share\cmake-3.xx\Modules

2.2.1 問:自己編寫的FindXXX.cmake放哪里

??答:默認(rèn)find_package(xxx)會優(yōu)先從cmake的Modules目錄查找,意味著我們可以把自己的FindXXX.cmake放到cmake的Modules目錄,但更優(yōu)雅的方式是跟著項目走。在沒有集成vcpkg的情況下,我們可以在項目根目錄創(chuàng)建一個cmake目錄,并將各種編寫的FindXXX.cmake放于此處,隨后需要在項目的CMakeList.txt里告知FindXXX.cmake所在目錄,即:list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake"), 當(dāng)然有了vcpkg就簡單多了,只要為此庫創(chuàng)建一個獨立的倉庫,并將FindXXX.cmake直接放于其中,后續(xù)通過vcpkg將其install即可。

2.2.2 問:如何編寫FindXXX.cmake

??答:其實,FindXXX.cmake本質(zhì)不一定要寫,因為FindXXX.cmake的主要目的是通過find_libraryfind_path指定庫的頭文件和binary所在路徑,但因為很多時候第三方庫往往有很多頭文件很多庫文件而且還分debug/release,不能像下面這種方式簡單描述,因此有必要提供一個獨立的文件來描述庫是怎么尋找和定義的,這樣能讓庫尋找庫使用完全分離解耦。

find_path(TIFF_INCLUDE_DIR tiff.h
    /usr/local/include
    /usr/include)
    
find_library(TIFF_LIBRARY
    NAMES tiff tiff2
    PATHS /usr/local/lib /usr/lib)

include_directories(${TIFF_INCLUDE_DIRs})
add_executable(mytiff mytiff.c)
target_link_libraries(myprogram ${TIFF_LIBRARY})

因為實際的編寫過程會很復(fù)雜,取決于不同形式的庫,因此下面單獨章節(jié)描述如何編寫FindXXX.cmake.

3. 如何編寫FindXXX.cmake

一個合格完整的FindXXX.cmake包含以下3個部分:

  1. 定義XXX_INCLUDE_DIRSXXX_LIBRARIESfind_path()每次只能獲得一個頭文件所在路徑,對于有很多頭文件的庫,需要通過多次find_path找到各自路徑,并將它們合并為XXX_INCLUDE_DIRS, 如果一個庫有很多庫文件,那么也需要多次find_library()找到各個庫對應(yīng)的路徑,并將其合并為XXX_LIBRARIES;
  2. 定義XXX_FOUNDXXX_VERSION: 確認(rèn)XXX_INCLUDE_DIRSXXX_LIBRARIES都不為空,再定義XXX_FOUNDXXX_VERSION。至此,library已經(jīng)可以被大幅簡化集成,只是集成時候需要導(dǎo)入XXX_INCLUDE_DIRS作為庫頭文件,鏈接 XXX_LIBRARIES作為庫文件,如果庫區(qū)分Debug和Release,那么cmake還要以optimize和debug方式依賴對應(yīng)的庫;
  3. 創(chuàng)建Target: 確認(rèn)XXX_FOUND不為空后再創(chuàng)建Target,通過add_library()定義庫類型(SHARED|STATIC|INTERFACE), 通過set_target_properties()設(shè)置LIB的頭文件路徑、靜態(tài)庫地址、動態(tài)庫地址、共享庫的地址以及DLL路徑。至此,庫的集成簡易程度已和源碼庫完全一樣。

在寫FindXXX.cmake前需要分析提供的第三方庫的特性,根據(jù)不同的特性將會采取不同的方式編寫FindXXX.cmake

  1. 是否單個頭文件或者單個庫文件:相對來說,單個頭文件和庫文件的庫寫FindXXX.cmake會簡潔很多,一個find_pathfind_library就能描述所有的依賴關(guān)系;
  2. 庫文件是否區(qū)分debug和release:只有windows庫才有可能區(qū)分debug和release,如果區(qū)分意味著需要讓cmake能動態(tài)找到對應(yīng)版本的庫文件;
  3. windows庫除了靜態(tài)庫是否還有動態(tài)庫:在定義Target時候,需要在property里設(shè)置靜態(tài)庫和動態(tài)庫的文件路徑

3.1 單頭文件&單庫文件&單dll的情況

# FindOpenCV
# --------
#
# Find the opencv libraries
#
# Result Variables
# ^^^^^^^^^^^^^^^^
#
# The following variables will be defined:
#
# ``opencv_FOUND`` True if opencv found on the local system
#
# ``opencv_VERSION`` Version of opencv found
#
# ``opencv_INCLUDE_DIRS`` Location of opencv header files
#
# ``opencv_LIBRARIES`` List of the opencv libraries found
#

find_package(PkgConfig)

# ======================= define XXX_ROOT_DIR =======================
if (DEFINED ENV{LOCAL_REPOSITORY})
    set(opencv_ROOT_DIR $ENV{LOCAL_REPOSITORY})
endif()

if (DEFINED ENV{VCPKG_ROOT} AND DEFINED ENV{VCPKG_DEFAULT_TRIPLET})
    set(opencv_ROOT_DIR $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_DEFAULT_TRIPLET})
endif()

# ======================= find header files =======================
find_path(opencv_INCLUDE_DIR
    NAMES opencv2/opencv.hpp
    PATHS ${opencv_ROOT_DIR}/include /usr/local/include)

# ======================= find library files =======================
# define macro func to find libs
macro(opencv_FIND_LIBRARY libname)
    if(NOT opencv_${libname}_LIBRARY)
        find_library(opencv_${libname}_LIBRARY
            NAMES ${libname}
            PATHS ${opencv_ROOT_DIR}/lib /usr/local/lib)

        list(APPEND opencv_LIBRARY ${opencv_${libname}_LIBRARY})
    endif()
endmacro(opencv_FIND_LIBRARY)

if(WIN32)
    find_library(opencv_LIBRARY_DEBUG
        NAMES opencv_world412d.lib
        PATHS ${opencv_ROOT_DIR}/debug/lib /usr/local/lib)

    find_library(opencv_LIBRARY_RELEASE
        NAMES opencv_world412.lib
        PATHS ${opencv_ROOT_DIR}/lib /usr/local/lib)

    include(SelectLibraryConfigurations)
    select_library_configurations(opencv)
elseif(UNIX)
    # call macro func to find libs
    opencv_FIND_LIBRARY(libopencv_core.so)
    opencv_FIND_LIBRARY(libopencv_cudaarithm.so)
    opencv_FIND_LIBRARY(libopencv_cudafilters.so)
    opencv_FIND_LIBRARY(libopencv_cudaimgproc.so)
    opencv_FIND_LIBRARY(libopencv_highgui.so)
    opencv_FIND_LIBRARY(libopencv_imgcodecs.so)
    opencv_FIND_LIBRARY(libopencv_imgproc.so)
endif()

# ======================= find bin files =======================
if(WIN32)
    find_file(opencv_LIBRARY_DLL_DEBUG
        NAMES opencv_world412d.dll
        PATHS ${opencv_ROOT_DIR}/debug/bin)

    find_file(opencv_LIBRARY_DLL_RELEASE
        NAMES opencv_world412.dll
        PATHS ${opencv_ROOT_DIR}/bin)
endif()

# ======================= verify dependencies =======================
if (opencv_INCLUDE_DIR AND opencv_LIBRARY)
    set(opencv_FOUND TRUR CACHE BOOL "")
    set(opencv_VERSION "4.1.2" CACHE STRING "")

    set(opencv_INCLUDE_DIRS ${opencv_INCLUDE_DIR} CACHE STRING "")
    set(opencv_LIBRARIES ${opencv_LIBRARY} CACHE STRING "")

    find_package_handle_standard_args(opencv
        REQUIRED_VARS opencv_INCLUDE_DIRS opencv_LIBRARIES
        VERSION_VAR opencv_VERSION)
    mark_as_advanced(opencv_INCLUDE_DIRS opencv_LIBRARIES)
endif()

# ======================= create target =======================
if (opencv_FOUND)
    include(CMakePushCheckState)
    cmake_push_check_state()

    # set required properties
    set(CMAKE_REQUIRED_QUIET ${opencv_FIND_QUIETLY})
    set(CMAKE_REQUIRED_INCLUDES ${opencv_INCLUDE_DIRS})
    set(CMAKE_REQUIRED_LIBRARIES ${opencv_LIBRARIES})

    cmake_pop_check_state()

    if(NOT TARGET opencv)
        add_library(opencv SHARED IMPORTED)
        set_target_properties(opencv PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${opencv_INCLUDE_DIRS}")

        if(opencv_LIBRARY_DEBUG)
            set_property(TARGET opencv APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
            set_target_properties(opencv PROPERTIES
                IMPORTED_LOCATION_DEBUG "${opencv_LIBRARY_DLL_DEBUG}"
                IMPORTED_IMPLIB_DEBUG "${opencv_LIBRARY_DEBUG}")
        endif()

        if(opencv_LIBRARY_RELEASE)
            set_property(TARGET opencv APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
            set_target_properties(opencv PROPERTIES
                IMPORTED_LOCATION_RELEASE "${opencv_LIBRARY_DLL_RELEASE}"
                IMPORTED_IMPLIB_RELEASE "${opencv_LIBRARY_RELEASE}")
        endif()

        if(NOT opencv_LIBRARY_RELEASE AND NOT opencv_LIBRARY_DEBUG)
            set_property(TARGET opencv APPEND PROPERTY IMPORTED_LOCATION "${opencv_LIBRARY}")
        endif()
    endif()
endif()

備注1${opencv_ROOT_DIR}指向的庫目錄是動態(tài)的,如果定義了VCPKG_ROOT, 那么vcpkg就是庫的尋找源;如果未定義VCPKG_ROOT但定義了LOCAL_REPOSITORY, 那么本地目錄即為庫尋找源;若都沒有定義,那么頭文件和庫文件就只能從系統(tǒng)路徑尋找了。不管當(dāng)前是哪個平臺,如:x64-windows、x86-windows、arm64-linux、x64-linux等等,${opencv_ROOT_DIR}下一般目錄結(jié)構(gòu)都是: includelib以及bin。

3.1 批量尋找頭文件: 模板化find_path()

因為find_path每次只能尋找一個頭文件,需要多次調(diào)用將最終結(jié)果合并為XXX_INCLUDE_DIRS。其實,也可以如下通過定義宏或者函數(shù)批量尋找頭文件:

# ======================= find header files =======================
# define macro func to find headers
macro(baumer_FIND_INCLUDE varname foldername headername)
    if(NOT baumer_${foldername}_INCLUDE_DIR)
        find_path(baumer_${foldername}_INCLUDE_DIR
            NAMES ${foldername}/${headername}
            PATHS ${baumer_ROOT_DIR}/include /usr/local/include)

        list(APPEND baumer_INCLUDE_DIRS ${baumer_${foldername}_INCLUDE_DIR})
        list(REMOVE_DUPLICATES baumer_INCLUDE_DIRS)
    endif()
endmacro(baumer_FIND_INCLUDE)

# call macro func to find headers
baumer_FIND_INCLUDE(bgapi2_ext            bgapi2_ext      bgapi2_ext.h)
baumer_FIND_INCLUDE(bgapi2_ext_addons     bgapi2_ext      bgapi2_ext_addons.h)
baumer_FIND_INCLUDE(bgapi2_ext_sc         bgapi2_ext_sc   bgapi2_ext_sc.h)
baumer_FIND_INCLUDE(bgapi2_def            bgapi2_genicam  bgapi2_def.h)
baumer_FIND_INCLUDE(bgapi2_featurenames   bgapi2_genicam  bgapi2_featurenames.h)
baumer_FIND_INCLUDE(bgapi2_genicam        bgapi2_genicam  bgapi2_genicam.hpp)

最終,baumer_INCLUDE_DIRS=E:\vcpkg\installed\x64-windows\include,你可能會疑惑那么多次的find_path定位到的include路徑都是一樣的,不是浪費么。No,其實目的是確保每個頭文件都能被find_path到。

以上腳本演示了將每個public的頭文件被尋找到并成為baumer_INCLUDES的一部分,庫調(diào)用者include頭文件則為#include <foldername/headername>,如:#include <bgapi2_ext/bgapi2_ext.h>

3.2 批量尋找?guī)煳募耗0寤痜ind_library()

find_path一樣find_library每次只能尋找一個庫文件,需要多次調(diào)用將最終結(jié)果合并為XXX_LIBRARIES。同樣也可以如下通過定義宏或者函數(shù)批量尋找?guī)煳募?/p>

# ---------------- find library files----------------
# define macro func to find libs
macro(baumer_FIND_LIBRARY libname)
  if(NOT baumer_${libname}_LIBRARY)
     find_library(baumer_${libname}_LIBRARY 
      NAMES ${libname} 
      PATHS ${baumer_ROOT_DIR}/lib /usr/local/lib)

     list(APPEND baumer_LIBRARIES ${baumer_${libname}_LIBRARY})
  endif()
endmacro(baumer_FIND_LIBRARY)

# call macro func to find libs
Baumer_FIND_LIBRARY(bgapi2_ext.lib)
Baumer_FIND_LIBRARY(bgapi2_ext_sc.lib)
Baumer_FIND_LIBRARY(bgapi2_genicam.lib)

最終,baumer_LIBRARIES=E:\vcpkg\installed\x64-windows\lib\bgapi2_ext.libE:\vcpkg\installed\x64-windows\lib\bgapi2_ext_sc.libE:\vcpkg\installed\x64-windows\lib\bgapi2_genicam.lib

3.3 尋找?guī)煳募?,但區(qū)分DEBUG和RELEASE

對于一些區(qū)分Debug和Release的windows庫,我們不能一味用xxx_LIBRARIES描述所有庫文件:

find_library(serial_LIBRARY_DEBUG
    NAMES seriald.lib
    PATHS ${serial_ROOT_DIR}/debug/lib /usr/local/lib)

find_library(serial_LIBRARY_RELEASE
    NAMES serial.lib
    PATHS ${serial_ROOT_DIR}/lib /usr/local/lib)

include(SelectLibraryConfigurations)
select_library_configurations(serial)

select_library_configurations(xxx)的引入使得cmake configure會自動選取對應(yīng)版本的庫賦值與xxx_LIBRARY, 意味著target_link_library時候不用區(qū)分optimize和debug的庫

至此,庫的集成將大幅簡化,因為可以做到了屏蔽不同路徑的頭文件不同路徑,不同名字的庫文件,甚至不用區(qū)分系統(tǒng)平臺:

find_package(XXX)
target_include_directory(app ${XXX_INCLUDE_DIRS})
target_link_library(app PRIVATE
     optimize ${XXX_LIBRARIES_RELEASE}
     debug ${XXX_LIBRARIES_DEBUG})

3.3 創(chuàng)建Target

為了徹底、完全做到跟源碼庫集成一樣簡潔:省略頭文件導(dǎo)入和不區(qū)分debug/release,我們可以手動創(chuàng)建Target。

3.3.1 只有單個static或者單個so的Target

if(NOT TARGET xxx)
    add_library(xxx SHARED|STATIC|UNKNOWN IMPORTED)
    set_target_properties(xxx PROPERTIES 
        INTERFACE_INCLUDE_DIRECTORIES "${xxx_INCLUDE_DIRS}"
        IMPORTED_LOCATION "${xxx_LIBRARY}")
endif()

add_library()時候可以指定library是SHARED庫、STATIC庫,如果不確定那種也可以填UNKNOWN,因為SHARED庫和STATIC庫的必要設(shè)置的property是一樣的

3.3.2 只有單個dll和單個lib的Target

if(NOT TARGET xxx)
    add_library(xxx SHARED IMPORTED)
    set_target_properties(xxx PROPERTIES 
        INTERFACE_INCLUDE_DIRECTORIES "${xxx_INCLUDE_DIRS}"
        IMPORTED_LOCATION "${xxx_LIBRARY_DLL}"
        IMPORTED_IMPLIB "${xxx_LIBRARY}")
endif()

3.3.3 有多個static和多個dll的Target

if(NOT TARGET xxx)
    add_library(xxx INTERFACE IMPORTED)
    set_target_properties(xxx PROPERTIES 
        INTERFACE_INCLUDE_DIRECTORIES "${xxx_INCLUDE_DIRS}"
        INTERFACE_LINK_LIBRARIES "${xxx_LIBRARIES}"
        IMPORTED_LOCATION "${xxx_LIBRARY_DLLS}")
endif()

3.3.4 區(qū)分Debug/Release的Target

if(NOT TARGET xxx)
    add_library(xxx STATIC IMPORTED)
    set_target_properties(xxx PROPERTIES INTERFACE_INCLUDE_DIRECTORIES "${xxx_INCLUDE_DIRS}")

    if(xxx_LIBRARY_DEBUG)
        set_property(TARGET xxx APPEND PROPERTY IMPORTED_CONFIGURATIONS DEBUG)
        set_target_properties(xxx PROPERTIES IMPORTED_LOCATION_DEBUG "${xxx_LIBRARY_DEBUG}")
    endif()

    if(xxx_LIBRARY_RELEASE)
        set_property(TARGET xxx APPEND PROPERTY IMPORTED_CONFIGURATIONS RELEASE)
        set_target_properties(xxx PROPERTIES IMPORTED_LOCATION_RELEASE "${xxx_LIBRARY_RELEASE}")
    endif()

    if(NOT xxx_LIBRARY_RELEASE AND NOT xxx_LIBRARY_DEBUG)
        set_property(TARGET xxx APPEND PROPERTY IMPORTED_LOCATION "${xxx_LIBRARY}")
    endif()
endif()

這是一個典型的兼容Windows和Linux的Target創(chuàng)建案例,因為很多時候Windows庫區(qū)分Debug和Release,而Linux的SO庫是不區(qū)分的。

至此,庫的集成將變?yōu)槿缦路绞剑?/p>

find_package(xxx REQUIRED)
target_link_library(app xxx)

4. vcpkg

??CMake提供了依賴庫尋找的功能,當(dāng)找不到依賴庫時只會報錯,并不會自動下載依賴庫,因為CMake不是真正的包管理器,頂多是一個包尋找器,所以得配合vcpkg,vcpkg是宗旨是即時編譯當(dāng)前或者指定平臺的庫,vcpkg內(nèi)部提供了眾多下載依賴途徑選項,如:http, ftp, gitlab, github, bitbucket,svn,cvs等等。

4.1 如何讓vcpkg和cmake結(jié)合讓庫依賴自動化

??vcpkg的詳細(xì)功能豐富,可自行查看官網(wǎng),這里只指導(dǎo)如何快速上手創(chuàng)建私有port,并讓vcpkg和cmake集成達(dá)到編譯即下載依賴庫的作用,我們分別以2種案例來介紹:

  • 基于源碼構(gòu)建的庫:相對更簡單
  • 基于只有頭文件和二進(jìn)制庫文件的第三方庫: 需要區(qū)分多平臺獨立,復(fù)雜一些

4.2 創(chuàng)建私有port

??所謂port就是一個以庫名為名字的目錄,里面放此庫的獲取來源配置版本及依賴關(guān)系配置、使用說明等,創(chuàng)建一個私有port的步驟如下:

  • 在你的C++項目根目錄創(chuàng)建一個文件夾叫vcpkg-ports, 如果你的庫名叫xxx,那么在vcpkg-ports里創(chuàng)建一個目錄叫xxx,那么這個xxx即是一個port。
  • 在port目錄里創(chuàng)建一個名為portfile.cmake的文本文件,在此配置文件里定義庫代碼或者二進(jìn)制獲取方式,以及相關(guān)配置。

portfile.cmake (以源碼方式將vcpkg和cmake集成):

set(SOURCE_PATH ${CURRENT_BUILDTREES_DIR}/src/${PORT})

vcpkg_from_git(
    OUT_SOURCE_PATH SOURCE_PATH
    URL ssh://git@lmsman-bitbucket01.europe.leicams.com:7999/lmswet-bitbucket01/~fei.zhang/smt-timer.git
    REF 9e950a8a52b7304e7da2ab59fd485f39095dca9b
    HEAD_REF master
)

# configure project and try to enable ninja
vcpkg_configure_cmake(
    SOURCE_PATH ${SOURCE_PATH} 
    PREFER_NINJA)

vcpkg_install_cmake()
vcpkg_copy_pdbs()

# relocate target to vcpkg
vcpkg_fixup_cmake_targets(
    CONFIG_PATH lib/cmake/${PORT} 
    TARGET_PATH /share/${PORT})

# remove headers in debug mode
file(REMOVE_RECURSE ${CURRENT_PACKAGES_DIR}/debug/include)

# install license and copyright
file(
    INSTALL ${SOURCE_PATH}/COPYING
    DESTINATION ${CURRENT_PACKAGES_DIR}/share/${PORT}
    RENAME copyright)

用已經(jīng)編譯好的二進(jìn)制庫的方式將vcpkg和cmake集成:

set(SOURCE_PATH "${CURRENT_BUILDTREES_DIR}/src/${PORT}")

vcpkg_from_git(
    OUT_SOURCE_PATH SOURCE_PATH
    URL git@10.10.231.59:fei.zhang/baumer.git
    REF 2d2d0a217da810f1ea71ca807c5337aed3e84e12
    HEAD_REF master
)

if (WIN32)
    # install headers
    file(GLOB_RECURSE HEADERS "${SOURCE_PATH}/windows/include/*")
    foreach(HEADER ${HEADERS})
         get_filename_component(ABSOLUTE_DIR ${HEADER} DIRECTORY)
         string(REGEX MATCH "include.+" NODE_DIR ${ABSOLUTE_DIR})
         file(INSTALL ${HEADER} DESTINATION "${CURRENT_PACKAGES_DIR}/${NODE_DIR}")
    endforeach()

    # install libs
    file(GLOB_RECURSE LIBRARIES "${SOURCE_PATH}/windows/lib/*")
    file(INSTALL ${LIBRARIES} DESTINATION "${CURRENT_PACKAGES_DIR}/lib")

    # install dlls
    file(GLOB_RECURSE BINS "${SOURCE_PATH}/windows/bin/*")
    file(INSTALL ${BINS} DESTINATION "${CURRENT_PACKAGES_DIR}/bin/${PORT}")
elseif (UNIX)
    # install headers
    file(GLOB_RECURSE HEADERS "${SOURCE_PATH}/linux/include/*")
    foreach(HEADER ${HEADERS})
         get_filename_component(ABSOLUTE_DIR ${HEADER} DIRECTORY )
         string(REGEX MATCH "include.+" NODE_DIR ${ABSOLUTE_DIR})
         file(INSTALL ${HEADER} DESTINATION "${CURRENT_PACKAGES_DIR}/${NODE_DIR}")
    endforeach()

    # install libs
    file(GLOB_RECURSE LIBRARIES "${SOURCE_PATH}/linux/lib/*")
    file(INSTALL ${LIBRARIES} DESTINATION "${CURRENT_PACKAGES_DIR}/lib")
endif()

# install FindXXX.cmake allow App can find pakcage via find_package(xxx)
file(INSTALL ${SOURCE_PATH}/FindBaumer.cmake DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")

# relocate the directory for cmake to find FindXXX.cmake
file(INSTALL ${CMAKE_CURRENT_LIST_DIR}/vcpkg-cmake-wrapper.cmake DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")

# install cmake integration usage
file(INSTALL ${CMAKE_CURRENT_LIST_DIR}/usage DESTINATION "${CURRENT_PACKAGES_DIR}/share/${PORT}")

# allow incomplete packages to pass validation
set(VCPKG_POLICY_EMPTY_PACKAGE enabled)
  • 在此port目錄里創(chuàng)建一個名為vcpkg.json的文本文件,在此配置里定義當(dāng)前庫的基礎(chǔ)信息以及依賴關(guān)系等,如下以swc-cameravcpkg.json為例(如果此庫沒有其他任何依賴且沒有版本,可以不用提供):
{
    "name": "swc-camera",
    "version-string": "v1.0.0",
    "description": "swc-camera is an integrated camera feature sdk",
    "dependencies": [
        "protocol",
        "smt-logger",
        "smt-timer",
        "opencv",
        "baumer",
        "tucsen",
        "gtest"
    ]
}

4.3 如何通過vcpkg自動下載庫和依賴庫

swc-camera為例,安裝命令為: vcpkg install swc-camera --overlay-ports=vcpkg-ports
當(dāng)看到如下過程log,意味著下載swc-camera以及其依賴庫已經(jīng)開始工作了

--port-overlay=vcpkg-ports 作用是指定vcpkg安裝庫,庫的port配置來自vcpkg-ports目錄,如果項目不依賴任何私有托管的倉庫,則不用指定--port-overlay

PS E:\vcpkg> .\vcpkg.exe install swc-camera --overlay-ports=vcpkg-ports
Computing installation plan...
The following packages will be built and installed:
  * baumer[core]:x64-windows -> v1.0.0 -- E:\vcpkg\vcpkg-ports\baumer
  * opencv[core]:x64-windows -> v4.1.2 -- E:\vcpkg\vcpkg-ports\opencv
  * smt-logger[core]:x64-windows -> v1.0.0 -- E:\vcpkg\vcpkg-ports\smt-logger
  * smt-timer[core]:x64-windows -> v1.0.0 -- E:\vcpkg\vcpkg-ports\smt-timer
    swc-camera[core]:x64-windows -> v1.0.0 -- E:\vcpkg\vcpkg-ports\swc-camera
  * tucsen[core]:x64-windows -> v1.0.0 -- E:\vcpkg\vcpkg-ports\tucsen
Additional packages (*) will be modified to complete this operation.
Detecting compiler hash for triplet x64-windows...

當(dāng)最后出現(xiàn)如下信息,意味著swc-camera安裝成功:

Total elapsed time: 16.2 s

The package swc-camera:x64-windows provides CMake targets:

    find_package(swc-camera CONFIG REQUIRED)
    target_link_libraries(main PRIVATE swc-camera)

4.4 如何通過vcpkg卸載庫

swc-camera為例,卸載命令為: vcpkg remove swc-camera --port-overlay=vcpkg-ports

需要注意的是remove不會將依賴庫一同卸載,因為cmake庫依賴都是引用依賴,不是包含依賴,你不用的庫可能別的項目在用。

4.5 如何將vcpkg集成到cmake項目中

# use local repository if defined
if (DEFINED ENV{LOCAL_REPOSITORY})
  set(CMAKE_PREFIX_PATH $ENV{LOCAL_REPOSITORY})
endif()

# preferred to use vcpkg if defined
if(DEFINED ENV{VCPKG_ROOT})
    set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake")
endif()

---------------------------------------------------------------------

project(testApp)
find_package(smt-logger)
add_executable(${PROJECT_NAME} main.cpp)
target_link_libraries(${PROJECT_NAME} smt-logger)

如上,演示了如何集成vcpkg到cmake項目,同時也指定了本地統(tǒng)一庫尋找目錄,且有尋找優(yōu)先級。

  • 首先,find_package()默認(rèn)優(yōu)先會嘗試從vcpkg里尋找,假設(shè)系統(tǒng)環(huán)境變量定義了vcpkg根目錄 —— VCPKG_ROOT
  • 隨后,若VCPKG_ROOT 未定義(假設(shè)你不喜歡vcpkg,想自己折騰),則嘗試從本地統(tǒng)一庫尋找目錄里尋找,假設(shè)系統(tǒng)環(huán)境變量定義了統(tǒng)一庫尋找目錄 —— LOCAL_REPOSITORY
  • 如果以上環(huán)境變量都沒有,那么則嘗試找默認(rèn)的路徑,linux從/usr/local里找,windows從C:/Program File里找
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容