C/C++的依赖管理算是非常凌乱的,在前面的文章中我们也算是大致介绍了下使用cmake管理依赖的方式.很明显这个方式痛点不少,还是比较繁琐复杂的.一旦一个项目依赖上十几二十个库那维护起来还是很酸爽的.同时如果第三方库大量使用源码安装会大大拖慢编译时间,造成大量的资源浪费.依赖大量托管在git仓库,http服务器上也会对编译的可维护性造成一定影响,同时对代码安全有要求的场景这种方式也很难管理维护.
我们期望的是可以有一个集中式的依赖仓库,类似pypi,它里面有大量常用库在各个常用平台上已经编译好的二进制包,同时也提供源码包以供在不常用的平台上也可以尝试安装.同时这个平台也提供一个客户端工具,类似pip,可以用于管理项目的依赖项,同时也支持将自己的库上传到仓库中统一管理,同时最好提供用户自行部署仓库的能力,以方便企业或者团体私人使用. C/C++有这样的工具么?答案是有,而且不止一个,但又不完全有.目前没有一个工具可以满足上面所有的期望,但又有两个候选的工具他们可以一定程度上满足上面的需求.他们就是vcpkg
和conan
,下面是它们以及各种linux发行版包管理工具对对比矩阵
特性\工具 | linux发行版包管理仓库 | vcpkg | conan |
---|---|---|---|
集中式管理 | 是 | 是 | 是 |
跨平台 | 否 | 是 | 是 |
常用平台提供二进制包 | 是 | 否 | 是 |
保管库数量 | 多 | 多 | 少 |
支持自行部署仓库 | 否 | 否 | 是 |
客户端工具用于查找安装卸载依赖 | 是 | 是 | 是 |
客户端工具可以安装指定版本的依赖 | 是 | 否 | 是 |
客户端工具用于发布依赖 | 是 | 是 | 是 |
客户端工具提供系统级依赖管理 | 是 | 否 | 否 |
客户端工具提供项目级的依赖管理 | 否 | 否 | 是 |
看起来conan更有优势,但架不住conan上的资源太少.好在这两个工具都可以无缝的和cmake结合使用,这样相当于我们可以使用cmake来管理项目,而不同的依赖管理工具则专门用于管理项目依赖.
任何情况下我们都希望我们编译项目不会影响系统,因此项目级的依赖管理才是我们应该采取的策略.
C/C++项目的依赖管理可以遵从如下规则进行:
- 尽可能使用
conan
管理项目依赖 conan
中找不到的包尽量使用cmake的FetchContent
模块从源码开始编译conan
中找不到的依赖,而且FetchContent
获取困难时可以尝试使用vcpkg
安装.- 尽量不用linux发行版包管理工具管理依赖
我们继续改造helloworld_with_log
项目.这个项目依赖spdlog
这个包而conan中刚好有.这里就借着这个项目介绍下如何使用conan构造可执行文件.
使用conan管理可执行文件项目的依赖只有5步
-
设置一些必要的环境变量和设置(根据平台情况设置)
主要要设置这些环境变量
CC=/usr/bin/gcc
CXX=/usr/bin/g++
CMAKE_C_COMPILER=gcc
CMAKE_Fortran_COMPILER=gfortran
conan则主要要设置它名为
default
的profile
.所谓profile
可以理解为编译环境设置,通过conan profile detect
创建,通过conan profile update <key>=<value> <profile_name>
设置.默认位置在~/.conan2/profiles/default
文件中保存可以设置的项可以在这里查到
通常我们比较关系如下几个设置项:
- settings.compiler=gcc
- settings.compiler.version=10
- settings.compiler.exception=seh
- settings.compiler.libcxx=libstdc++11
- settings.compiler.threads=posix
- settings.build_type=Release
-
项目根目录编写conanfile.txt用于描述依赖.
在
conanfile.txt
中通常只要设置好requires
和generators
即可.其中
requires
中以<package_name>/<version>
的形式描述依赖,我们可以通过conan search <package_name>[ -r <REMOTE>]
来查找conan仓库中托管的依赖包.默认会从conancenter中查找包.我们也可以用-r
来指定已经设置过的conan仓库,私有仓库一块我们后面介绍.其中
generators
目前基本都是填的[generators] CMakeDeps CMakeToolchain
-
创建
build
文件夹,执行conan install . --output-folder=build [ --build=missing[ --profile=<profile_name>[ -r <REMOTE>]]]
安装conan install
命令用于安装并配置项目的依赖.默认只会下载当前平台已经编译好的二进制库,如果碰到二进制库有缺失就会失败,我们可以加上--build=missing
让它碰到有缺失的库时尝试现编译.而--profile
则是指定特定的编译配置项 -
在项目根目录创建你的cmake配置文件
CmakeLists.txt
,并在其中加入对conan管理包的引用:... find_package(<依赖包名> REQUIRED) ... target_link_libraries(${PROJECT_NAME} ... <依赖包名>::<依赖包名> )
具体怎么导入依赖,我们通常可以在
conancenter
上对应仓库的说明中找到 -
在
build
文件夹下,根据执行操作系统的不同,执行如下命令:-
windows下:
cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake cmake --build . --config Release
-
linux/macos下
cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DCMAKE_BUILD_TYPE=Release cmake --build .
-
这样就编译完成了
我们修改前面的helloworld_with_log
继续做例子,这个例子在https://github.com/hsz1273327/TutorialForCLang/tree/master/examples/%E5%B7%A5%E5%85%B7%E9%93%BE/%E7%BC%96%E8%AF%91%E5%B7%A5%E5%85%B7%E9%93%BE/%E4%BE%9D%E8%B5%96%E7%AE%A1%E7%90%86/helloworld_with_log看到
源码不用修改,只是增加conanfile.txt
并修改CmakeLists.txt
即可.
-
conanfile.txt
[requires] spdlog/1.14.1 [generators] CMakeDeps CMakeToolchain
-
CmakeLists.txt
#项目编译环境 cmake_minimum_required (VERSION 3.17) # # 确保可以描述项目 # if (POLICY CMP0048) # cmake_policy(SET CMP0048 NEW) # endif (POLICY CMP0048) project (helloworld_with_log VERSION 0.0.0 DESCRIPTION "简单测试" LANGUAGES CXX ) # 引入conan维护的依赖 message(NOTICE "引入conan维护的依赖") # include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake) # conan_basic_setup() find_package(spdlog REQUIRED) option(AS_STATIC "是否作为纯静态可执行文件编译" off) # 构造可执行文件的配置 add_executable(${PROJECT_NAME}) ## 设置源码位置 target_sources(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/helloworld.cc ) target_link_libraries(${PROJECT_NAME} spdlog::spdlog ) if(AS_STATIC) message("编译纯静态可执行文件") target_compile_options(${PROJECT_NAME} PRIVATE "-static" ) endif()
前面也说过很多包conan中是找不到的,这种情况下我们希望尽量在不动系统库的情况下控制依赖,怎么办呢?
一个最通用的方法是使用cmake自带的FetchContent
模块直接在源头上管理依赖,这也是我更加推荐的方式.但有时候真的很麻烦.另一种选择是使用vcpkg先安装好依赖,然后把这个依赖融合到CmakeLists.txt
中.
vcpkg
安装依赖很简单,就是cvpkg install <package_name>[:TRIPLET]
,TRIPLET可以大致的和前面conan的profile
划等号,它也是一组编译设置,放在安装目录的triplets
文件夹下.
我们结合vcpkg的默认例子,改造上面的helloworld_with_log
,让它打印用vckpg安装的sqlite3的版本号.这个例子在https://github.com/hsz1273327/TutorialForCLang/tree/master/examples/%E5%B7%A5%E5%85%B7%E9%93%BE/%E7%BC%96%E8%AF%91%E5%B7%A5%E5%85%B7%E9%93%BE/%E4%BE%9D%E8%B5%96%E7%AE%A1%E7%90%86/hello_sqlite
这个例子中我们的源码文件hello_sqlite.cpp
为
#include <sqlite3.h>
#include <spdlog/spdlog.h>
int main() {
spdlog::info(sqlite3_libversion());
return 0;
}
这个例子我们需要先用vcpkg install sqlite3
安装号sqlite3.然后修改CmakeLists.txt
#项目编译环境
cmake_minimum_required (VERSION 3.17)
SET(CMAKE_TOOLCHAIN_FILE "/vcpkg/vcpkg/scripts/buildsystems/vcpkg.cmake")
# 确保可以描述项目
if (POLICY CMP0048)
cmake_policy(SET CMP0048 NEW)
endif (POLICY CMP0048)
project (hello_sqlite
VERSION 0.0.0
DESCRIPTION "简单测试"
LANGUAGES CXX
)
# 引入conan维护的依赖
message(NOTICE "引入conan维护的依赖")
# 找到vcpkg安装的sqlite3
find_package(unofficial-sqlite3 CONFIG REQUIRED)
# 找到conan安装的spdlog
find_package(spdlog REQUIRED)
option(AS_STATIC "是否作为纯静态可执行文件编译" off)
# 构造可执行文件的配置
add_executable(${PROJECT_NAME})
## 设置源码位置
target_sources(${PROJECT_NAME}
PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/hello_sqlite.cpp
)
target_link_libraries(${PROJECT_NAME}
PRIVATE unofficial::sqlite3::sqlite3
spdlog::spdlog
)
if(AS_STATIC)
message("编译纯静态可执行文件")
target_compile_options(${PROJECT_NAME}
PRIVATE "-static"
)
endif()