xmake:另一个C++现代构建系统

发布于 22 天前  81 次阅读



主要起因是我在逛Reddit帖子时,看到关于一些c++构建系统的评价. cmake似乎有些过于复杂,它与vcpkg,conan的包管理之间的"融合"可能在有些时候也显得麻烦. 一些人尝试了我没见过的选项,

所以这里主要试试除了cmake之外的构建工具(这些选项中个人目前看好xmake),除了xmake之外,还有The Meson Build systemAbout Spack - SpackBazel 简介 (google.cn)等等.我选择xmake主要原因是其自带的包管理和方便的写法

在进一步介绍xmake之前,有必要区分构建工具和包管理工具.c++目前常用的包管理工具有vcpkg,cpm以及conan,它们都有自己的registries,类似于node的npm,cargo的crates,python的pip registries,java的maven仓库. 都是社区或官方维护的库下载点,一般来允许用户注册后上传自己的包(不过考虑到c++生态是一堆轮子哥,用第三方库的人可能没有python,js一半多).

纯纯FetchContent

## from https://cmakebyexample.dev/use-library-fetchcontent/
cmake_minimum_required(VERSION 3.28)
project(my-project)

include(FetchContent)
FetchContent_Declare(
  cpr
  GIT_REPOSITORY https://github.com/libcpr/cpr.git
  # https://github.com/libcpr/cpr/releases
  GIT_TAG 1.10.4)
FetchContent_MakeAvailable(cpr)

add_executable(my-app main.cpp)
target_compile_features(my-app PUBLIC cxx_std_20)
target_link_libraries(my-app PRIVATE cpr::cpr)

利用cmake内置功能,下载相应库.它需要从源代码构建curl.

FetchContent 可让您直接在 CMake 项目中包含外部项目,从而更轻松地处理依赖关系,而无需用户单独下载和构建,或依赖系统级软件包管理器。它通过自动处理下载、构建和配置这些依赖项等任务,简化了将外部代码引入项目的过程。

include(FetchContent)

FetchContent_Declare(
    my_dependency
    GIT_REPOSITORY https://github.com/example/my_dependency.git
    GIT_TAG v1.0.0
)

FetchContent_MakeAvailable(my_dependency)

# Now you can use the components of 'my_dependency' in your project

vcpkg

包仓库Browse public vcpkg packages

mkdir myModule && cd myModule
vcpkg --new application # 创建应用

在目录下生成了两个文件,vcpkg.jsonvcpkg-confuguration.json分别用于存依赖信息和仓库信息.初始前者为空,后者

{
  "default-registry": {
    "kind": "git",
    "baseline": "9760ce6194ef51aa4faf77b6321e1280daa4545c",
    "repository": "https://github.com/microsoft/vcpkg"
  },
  "registries": [
    {
      "kind": "artifact",
      "location": "https://github.com/microsoft/vcpkg-ce-catalog/archive/refs/heads/main.zip",
      "name": "microsoft"
    }
  ]
}

默认的 vcpkg-configuration.json 文件引入了基线约束,指定了项目应使用的依赖项的最小版本,将 vcpkg-configuration.json 添加到代码控制中。

vcpkg add port fmt

添加依赖信息,这里并没有下载包,所以直接引入头文件会报错的

使用下面指令下载vcpkg.json中的包

vcpkg install

还会贴心提示在cmake中如何使用

image-20240911181040866

vcpkg官方推荐使用CMakePresets.json配置进行构建

使用预设,项目的顶层目录必须包含名为 CMakePresets.json 或 CMakeUserPresets.json 的文 件。若两个文件都存在,将先解析 CMakePresets.json,再解析 CMakeUserPresets.json。这两个文件 有相同的格式,但使用方式略有不同

cmake --listpresets # 列出写的预设
cmake --preset=name #选择某个预设

其要求写入缓存变量CMAKE_TOOLCHAIN_FILE就是本机安装的vcpkg.cmake,

如果你设置了VCPKG_ROOT环境变量,可以使用"$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"

{
  "version": 3,
  "configurePresets": [
    {
      "name": "default",
       "displayname": "default",
      "generator": "Ninja", # 构建工具
      "binaryDir": "${sourceDir}/build", # 构建输出目录
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "<VCPKG_ROOT>/scripts/buildsystems/vcpkg.cmake" #设置vcpkg工具链
      }
    }
  ]
}

version 字段指定要使用的 JSON 模式。版本 1 是 CMake 3.19 的第一个版本,只支持 configurePresets。版本 2 增加了 buildPresets 和 testPresets,CMake 3.20 开始支持; 版本 3 增加了 更多选项,CMake 3.21 开始支持。 可选的 cmakeMinimumRequired 字段可以用来定义构建此项目所需的 CMake 的最小版本。由于 最低要求通常也在 CMakeLists.txt 文件中说明,这通常会省略。 这三个列表:configurePresets、buildPresets 和 testPresets,每个列表都包含了用于配置、构建和 测试项目的配置。构建和测试的预置要求至少有一个配置预置,将在本节后面看到。 vendor 字段包含特定于供应商或 IDE 信息的可选映射。CMake 不解释该字段的内容

然后写上cmake文件,因为先使用preset会执行

cmake_minimum_required(VERSION 3.23)
project(HelloWorld)
find_package(fmt CONFIG REQUIRED) #重点 find_package找包
add_executable(HelloWorld main.cpp)
target_link_libraries(HelloWorld PRIVATE fmt::fmt) #进行链接

执行cmake --preset=name会进行构建,如果没有安装相应库,也会安装.所以上面的vcpkg install就没必要执行了.

我在使用时遇到了找不到实现的问题,这貌似需要设置vcpkg triplet,网上有一些triplets模板,简单来说需要设置一系列vcpkg变量

一个简单的方法时设置VCPKG_TARGET_TRIPLET变量为"x64-mingw-dynamic",表示使用mingw的动态库.或者使用

vcpkg install fmt:x64-mingw-dynamic

可以设置 VCPKG_TARGET_TRIPLET(需要下载和使用的库的架构)和VCPKG_HOST_TRIPLET(生成自己的库需要的构建架构)来设置target和host的架构,使用VCPKG_OVERLAY_TRIPLETS使用社区的.

vcpkg help triplets 

查看官方和社区提供的triplets.

Triplets

triplets指定平台架构,并按照对应的架构去设置toolchain,默认是在vcpkg/triplets下找一个符合平台的,比如vcpkg install xxx:abcd,其中abcd就是一个triplets,设置好triplet后清单模式下就去下载相应的包

用于交叉编译的,也就是在宿主机上编译不同平台架构的程序/库.

默认triplets

  • Windows: x64-windows
  • Linux: x64-linux
  • OSX: x64-osx

可以分别指定target-triplet和host-triplet设置对应需要的工具链名称,会根据对应的名字去vcpkg/triplets找,或者使用overlay-triplets覆盖目录

以上的问题可以总结为

  1. 使用clang需要配置triplets,可以使用社区的 How to compile vcpkg libraries using Clang? · Issue #38042 · microsoft/vcpkg (github.com) 使用覆盖三联密码 | Microsoft Learn Neumann-A/my-vcpkg-triplets: my collection of vcpkg triplets (github.com)

我注意到社区中的triplets中很多使用了VCPKG_CHAINLOAD_TOOLCHAIN_FILE指定要使用的备用 CMake 工具链文件,如果设置将替代所有其他编译器检测逻辑.默认情况下工具链文件是从 scripts/toolchains/ 适合平台选择的,使用--overlay-triplets选择一个目录进行覆盖使用覆盖三联密码 | Microsoft Learn

CMake 使用工具链实用程序来编译、link libraries和创建archives,并执行其他任务来进行构建。

可用的工具链由启用的语言决定。在正常编译中,CMake 会根据系统自省和默认值自动为主机编译确定工具链。在交叉编译情况下,可指定一个包含编译器和工具路径信息的工具链文件

下面信息很重要,cmake在project()指令处查找可用的工具链,在CMAKE_TOOLCHAIN_FILE中处理vcpkg逻辑,其中将默认调用合适的vckpg/scripts/toolchains/中的toolchain

  1. 在windows上使用vcpkg安装得到的动态库如果想要自己放在一个统一的3rdparty目录,include目录和bin目录中再自己设置link

需要放在可执行程序目录下,可以通过设置路径解决

CMake+MinGW+vcpkg项目引入三方库的两种方式(手动路径,vcpkg)-CSDN博客

c++ - cmake add_library with set_property can not found fmt dll on windows - Stack Overflow注意需要设置链接的.lib和.dll库,通过IMPORTED_IMPLIB设置输出的.lib库

  • IMPORTED_LOCATION:用于存储导入库的实际文件路径,通常是共享库或可执行文件的位置。
  • IMPORTED_IMPLIB:用于存储导入库(导入库 .lib 文件)的路径,通常在 Windows 平台上使用。
cmake_minimum_required(VERSION 3.10)

project(HelloWorld)
message("CMAKE_SOURCE_DIR: ${CMAKE_SOURCE_DIR}")

add_executable(HelloWorld main.cpp)
add_library(fmt SHARED IMPORTED)
set_property(TARGET fmt PROPERTY IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/3rdparty/fmt_x64-mingw-dynamic/bin/libfmt.dll)
# 改变导入库的.dll的位置
target_include_directories(fmt INTERFACE ${CMAKE_SOURCE_DIR}/3rdparty/fmt_x64-mingw-dynamic/include)

target_link_libraries(HelloWorld fmt)
add_custom_command(TARGET HelloWorld POST_BUILD     #-for copy libs in windows
            COMMAND ${CMAKE_COMMAND} -E copy_if_different 
            ${CMAKE_SOURCE_DIR}/3rdparty/fmt_x64-mingw-dynamic/bin/libfmt.dll 
            ${CMAKE_SOURCE_DIR}/build)
            # 将输出动态库放在build目录下

需要添加设置IMORTED_IMPLIB路径

add_library(fmt SHARED IMPORTED)
set_property(TARGET fmt PROPERTY
  IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/3rdparty/fmt_x64-mingw-dynamic/bin/libfmt.dll
  IMORTED_IMPLIB ${CMAKE_SOURCE_DIR}/3rdparty/fmt_x64-mingw-dynamic/lib/libfmt.lib
)

注意,使用vcpkg自动引入需要设置build_type,这样第三方库的动态库.dll就放在对应位置不用改了.

使用vcpkg时注意,

  1. 设置CMAKE_TOOLCHAIN_FILE要在project指令之前
  1. 使用find_package(xxx CONFIG REQUIRED)
  2. 推荐使用preset
  3. 设置CMAKE_EXPORT_COMPILE_COMMANDS为ON让clangd检测头文件目录
cmake -B build -S /my/project --preset debug

除了下载包之外,也有打包、发布等常用功能,已经满足常用需求了,cmake本身有cpack,但没有vcpkg提供的一些发布功能

要创建vcpkg包,首先创建manifest清单文件

{
  "name": "libogg",
  "version-string": "1.3.3",
  "description": "Ogg is a multimedia container format, and the native file and stream format for the Xiph.org multimedia codecs."
}

在项目vcpkg.json中写上包内容,然后创建portfile.cmake,修改下面的配置

vcpkg_from_github(
    OUT_SOURCE_PATH SOURCE_PATH
    REPO xiph/ogg
    REF v1.3.3
    SHA512 0bd6095d647530d4cb1f509eb5e99965a25cc3dd9b8125b93abd6b248255c890cf20710154bdec40568478eb5c4cde724abfb2eff1f3a04e63acef0fbbc9799b
    HEAD_REF master
)

用于GitHub存储库路径的REPO,用于使用稳定标签/提交的REF,以及带有下载文件校验和的SHA512,别人使用时也使用vcpkg下载

vcpkg_cmake_configure(SOURCE_PATH ${SOURCE_PATH})
vcpkg_cmake_install()
file(INSTALL "${SOURCE_PATH}/COPYING" DESTINATION "${CURRENT_PACKAGES_DIR}/share/libogg" RENAME copyright)

缺点就是微软文档实在烂,不是翻译的问题,英文原文的用词像是东拼西凑,感觉像是不停换人写出来的.

CPM.cmake

轻量的包管理工具cpm-cmake/CPM.cmake: 📦 CMake's missing package manager. A small CMake script for setup-free, cross-platform, reproducible dependency management. (github.com),基于cmake的FetchContent.

你只需要include提供的cpm.cmake文件即可

mkdir -p cmake
wget -O cmake/CPM.cmake https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake

此外也可以在cmake文件中下载

[...]

# download CPM.cmake
file(
  DOWNLOAD
  https://github.com/cpm-cmake/CPM.cmake/releases/download/v0.38.3/CPM.cmake
  ${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake
  EXPECTED_HASH SHA256=cc155ce02e7945e7b8967ddfaff0b050e958a723ef7aad3766d368940cb15494
)
include(${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake)

# add dependencies here
CPMAddPackage(...)

[...]

然后就能直接使用了,

CPMAddPackage(
  NAME          # The unique name of the dependency (should be the exported target's name)
  VERSION       # The minimum version of the dependency (optional, defaults to 0)
  PATCHES       # Patch files to be applied sequentially using patch and PATCH_OPTIONS (optional)
  OPTIONS       # Configuration options passed to the dependency (optional)
  DOWNLOAD_ONLY # If set, the project is downloaded, but not configured (optional)
  [...]         # Origin parameters forwarded to FetchContent_Declare, see below
)

来源可以由 GIT_REPOSITORY 指定,但也支持其他来源,如一般的URL。如果没有明确指定 GIT_TAG,则默认为 v(VERSION),这是 git 项目的通用约定。另一方面,如果没有明确指定 VERSION,在某些常见情况下,CPM 可以根据 git 标签自动识别版本。GIT_TAG 也可以设置为特定的提交或分支名称(如 master),但不建议这样做,因为这样的软件包只有在缓存被清除时才会被更新

如果将附加的可选参数 EXCLUDE_FROM_ALL 设为真,则默认情况下不会编译依赖关系中定义的任何目标.

# A git package from a given uri with a version
CPMAddPackage("uri@version")
# A git package from a given uri with a git tag or commit hash
CPMAddPackage("uri#tag")
# A git package with both version and tag provided
CPMAddPackage("uri@version#tag")

与findpackage差别,在 CMake 项目中添加库的通常方法是调用 find_package(\),并与 \_LIBRARIES 变量中定义的库进行链接。这种方法虽然简单,但可能会导致无法预测的编译,因为它需要在系统中安装库,而且不清楚添加的是哪个版本的库。此外,交叉编译项目(如mobile项目)也很困难,因为需要针对每个目标架构手动重建依赖关系。

CPM.cmake 允许明确定义依赖关系,并从源代码构建依赖关系。请注意,该行为不同于 find_package,因为使用 CPM.cmake 添加软件包后,导出到父作用域的变量(如 \_LIBRARIES )将不可见。如果需要,可以手动实现该行为find_package and CPMFindPackage have different behaviors · Issue #132 · cpm-cmake/CPM.cmake (github.com)

与fech_content差别,CPM.cmake 会检查任何添加的依赖项的版本号,如果另一个依赖项需要更新版,则会发出警告。
离线编译: CPM.cmake 将覆盖 CMake 的下载和更新命令,如果本地有所有依赖项,则可在离线状态下配置新的构建。
自动浅层克隆:如果提供了版本标签(如 v2.2.0)并使用了 CPM_SOURCE_CACHE,CPM.cmake 将执行依赖关系的浅层克隆,这应比完全克隆更快,同时使用的存储空间更少。可重写:通过设置 CMake 标志,所有 CPMAddPackage 都可配置为使用 find_package,从而轻松集成到可能需要通过系统的软件包管理器进行本地版本控制的项目中。软件包锁定文件可实现更简便的跨依赖关系管理。
每次编译时,可使用 CMake CLI 参数覆盖依赖关系。

cmake_minimum_required(VERSION 3.28)
project(my-project)
include(cmake/CPM.cmake)
CPMAddPackage("gh:fmtlib/fmt#10.2.0")
add_executable(my-app main.cpp)
target_compile_features(my-app PRIVATE cxx_std_20)
target_link_libraries(my-app fmt::fmt)

上面的就使用CPMAddPackage添加了github上的fmtlib作者的fmt库,版本10.2.0

更多例子CPM.cmake/examples at master · cpm-cmake/CPM.cmake (github.com)

CONAN

这里做简单介绍,conan有仓库中心Conan 2.0: C and C++ Open Source Package Manager

pip install conan

在项目中创建conanfile.txt,

[requires]
zlib/1.2.11
[generators]

CMakeDeps CMakeToolchain

generators表示将使用CMakeDeps来生成关于Zlib库文件安装位置的信息,CMakeToolchain通过CMake工具链文件传递构建信息给CMake

此外还要创建conan profile,里面提供本机系统信息,

conan profile detect --force
image-20240912092357159

生成的信息可以修改,比如改编译器

 conan install . --output-folder=build --build=missing

安装库,在build目录下会生成preset和conan_toolchain.cmake文件

cmake_minimum_required(VERSION 3.15)
project(compressor C)

find_package(ZLIB REQUIRED)

add_executable(${PROJECT_NAME} src/main.c)
target_link_libraries(${PROJECT_NAME} ZLIB::ZLIB)

再写个cmake文件,链接库

cd build
# assuming Visual Studio 15 2017 is your VS version and that it matches your default profile
cmake .. -G "Visual Studio 15 2017" -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake"
cmake --build . --config Release

利用toolchain文件导入包.

上面讲的都是包管理工具(实际上脱离不了相应的构建工具),最大的问题就是两者分开了,但事实上完全可以放在一起,就像rust的cargo,cargo build,cargo add

xmake

xmake包括了自带的构建工具和包管理,也有项目创建工具的功能.xmake使用lua配置. xmake create创建项目,可以选择语言和模板(竟然还包括zig)

xmake create --help
xmake create -P hello

创建好后目录下有xmake.lua,使用xmake即可直接构建.xmake run hello运行程序,

xmake.lua中常见配置就是创建可执行程序,库,添加头文件,设置语言标准等.

target("library")
    set_kind("shared") # 设置生成的类型
    add_files("src/library/*.c")

target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_deps("library") # 添加依赖

此外add_headerfiles添加头文件,set_languages设置语言版本.

xmake f -p表示配置架构

xmake config -m debug xmake`
xmake run -d hello

可以进行debug

xmake f -p [macosx|linux|iphoneos ..] -a [x86_64|i386|arm64 ..] -m [debug|release]

使用xmake配置项目,注意编译器设置

xmake f -p linux --sdk=/user/toolsdk --cxx=armv7-linux-clang++

我在windows上使用clang++工具,配置llvm工具链即可

xmake f -p cross --toolchain=llvm --sdk="C:\Program Files\LLVM" 
xmake
xmake run hello
xmake -v # 验证工具链配置

或者使用mingw

xmake f -p mingw

xmake官方有`xmake-repo仓库用于下载包,

add_requires("tbox 1.6.*", "libpng ~1.16", "zlib")

target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_packages("tbox", "libpng", "zlib")

上面配置从官方仓库中添加了依赖,执行xmake进行构建,会拉取相关的源包,然后自动编译安装,最后编译项目,并链接依赖包,此外还可以设置第三方依赖,从vcpkg,conan等地方

add_requires("vcpkg::zlib", "vcpkg::pcre2")

此外还有独立的xrepo可用于下载包,可以指定平台,版本等,支持从vcpkg,conan中搜索下载.

xrepo install zlib tbox 
xrepo install "zlib >=1.2.0" 
xrepo install -p iphoneos -a arm64 zlib
xrepo install vcpkg::zlib 
xrepo install conan::zlib/1.2.11 

我写了下面xmake.lua

add_rules("mode.debug", "mode.release")
add_requires("fmt")
target("hello")
    set_kind("binary")
    add_files("src/*.cpp")
    add_packages("fmt::fmt")

如果说vcpkg重点是选好triplet、toolchainfile,那么xmake也是配好toolchain

我直接配置xmake f -p windows使用msvc能成功build,如果要使用clang,配置好平台和工具链即可,相比vcpkg要好一些,vcpkg官方对clang/llvm貌似没有那么上心,官方triplet都没有,只能用社区的

xmake f -p windows  --toolchain=clang-cl 
xmake f -p windows  --toolchain=clang #或者

xmake show -l toolchains查看所有的toolchains

或者在xmake.lua中使用

set_plat(os.host())
set_arch(os.arch())
set_toochains("clang")
# 或者直接统一设置
set_toochains("clang",{palt=os.host(),arch=os.arch()})

目前还存在头文件无法识别的问题

image-20240912121858364
xmake project -k compile_commands

参考作者给出的解决支持为vscode-cpptools提供intellisense配置信息 · Issue #40 · xmake-io/xmake-vscode (github.com)生成compile_commands.json包含编译器,工作目录和头文件目录等信息.

xmake project -k vsxmake -m "debug,release" # New vsproj generator (Recommended)
xmake project -k vs -m "debug,release"
xmake project -k cmake
xmake project -k ninja
xmake project -k compile_commands

因为目前就langd和vscode的intellisense能检测文件中头文件目录信息等,需要这个文件

使用下来的体验就是写起来比cmake用的轻松,文档写的案例也比较全面.但是因为没有深度使用,一些细节不知道怎样.

总结

因为我已经在很多项目中使用了cmake,经验相对更多,xmake学习起来并不困难,搭配官方文档很容易搭建一个项目.

但目前还是推荐使用cmake+vcpkg/cpm.cmake方案,因为更成熟,解决方案更多.

届ける言葉を今は育ててる
最后更新于 2024-09-14