Xmake 和 C/C++ 包管理

waruqi發表於2022-03-12

Xmake 是一個基於 Lua 的輕量級跨平臺構建工具,關於 Xmake 與構建系統的介紹,我們已經在之前的文章中做了詳細的介紹:C/C++ 構建系統,我用 xmake

如果大家已經對 Xmake 已經有了大概的瞭解,就會知道,它不僅僅是一個構建工具,還內建了對 C/C++ 包管理的支援,我們也可以把 Xmake 理解為:

Xmake = Build backend + Project Generator + Package Manager

經過幾年的持續迭代,Xmake 對 C/C++ 包管理的支援不斷完善,也新增了不少實用的包管理特性,因此,在本文中,我們對其做一些總結,希望對大家有所幫助。

構建系統與包管理

C++ 的生態比較繁雜,這其中也有一定歷史原因,不管如何,官方沒有提供原生的包管理支援,對我們開發者來說,使用第三方 C++ 依賴庫多少存在很多不便。

其實,現在已經有很多強大的 C/C++ 包管理器,最知名,用的最多的有:vcpkg, conan, conda 等等,它們雖然很強大,但是有一個共同的問題:構建工具對它們沒有提供原生的支援。

由於 CMake 對它們沒有提供內建支援,想在 CMake 中使用它們整合依賴包非常繁瑣,並且整合和使用的方式都不一致。

在 CMake 中使用 Conan

在 CMake 中使用 conan 整合 C/C++ 包,我們需要提供額外的 CMake Wrapper 指令碼,以類似外掛的方式注入進自己的工程中去。

cmake_minimum_required(VERSION 3.5)
project(FormatOutput CXX)

list(APPEND CMAKE_MODULE_PATH ${CMAKE_BINARY_DIR})
list(APPEND CMAKE_PREFIX_PATH ${CMAKE_BINARY_DIR})

add_definitions("-std=c++11")

if(NOT EXISTS "${CMAKE_BINARY_DIR}/conan.cmake")
  message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan")
  file(DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake-conan/v0.16.1/conan.cmake"
                "${CMAKE_BINARY_DIR}/conan.cmake"
                EXPECTED_HASH SHA256=396e16d0f5eabdc6a14afddbcfff62a54a7ee75c6da23f32f7a31bc85db23484
                TLS_VERIFY ON)
endif()

include(${CMAKE_BINARY_DIR}/conan.cmake)

conan_cmake_configure(REQUIRES fmt/6.1.2
                      GENERATORS cmake_find_package)

conan_cmake_autodetect(settings)

conan_cmake_install(PATH_OR_REFERENCE .
                    BUILD missing
                    REMOTE conancenter
                    SETTINGS ${settings})

find_package(fmt)

add_executable(main main.cpp)
target_link_libraries(main fmt::fmt)

為了整合一個包,需要額外配置很多的指令碼。

在 CMake 中使用 Vcpkg

在 CMake 中使用 vcpkg 整合包,我們也需要額外注入一個工具鏈指令碼檔案。

cmake -B [build directory] -S . -DCMAKE_TOOLCHAIN_FILE=[path to vcpkg]/scripts/buildsystems/vcpkg.cmake
cmake --build [build directory]

另外,還有一個問題,就是我們還需要額外自己呼叫 vcpkg install [packages] 命令,去安裝包。

這其中每一個環節,對於使用者來講都需要額外的探索過程,沒法做到真正的一鍵編譯。

想象下,使用者下載了一個整合了 vcpkg 包的 cmake 專案,想要編譯通過,除了專案配置,還需要做哪些額外的事情:

  1. 安裝 vcpkg
  2. 執行 vcpkg install xxx 安裝裡面需要的包
  3. 執行 cmake 傳遞 vcpkg.cmake 指令碼給 cmake,進行工程配置

在 CMake 中使用 FetchContent

提供了 FetchContent 模式來管理依賴,但似乎是原始碼拉取,而且必須依賴也是基於 CMake 維護構建的,另外,我們需要對每個依賴項,配置 url, 版本等各種包資訊。

cmake_minimum_required(VERSION 3.14)
project(fetchContent_example CXX)

include(FetchContent)

FetchContent_Declare(
        DocTest
        GIT_REPOSITORY "https://github.com/onqtam/doctest"
        GIT_TAG "932a2ca50666138256dae56fbb16db3b1cae133a"
)
FetchContent_Declare(
        Range-v3
        GIT_REPOSITORY "https://github.com/ericniebler/range-v3"
        GIT_TAG "4d6a463bca51bc316f9b565edd94e82388206093"
)

FetchContent_MakeAvailable(DocTest Range-v3)

add_executable(${PROJECT_NAME} src/main.cpp)
target_link_libraries(${PROJECT_NAME} doctest range-v3)

在 Meson 中使用依賴包

Meson 很強大,並且也提供了自帶的包管理支援,但是想要在 Meson 中使用其他包管理器,例如 vcpkg/conan 等等同樣很繁瑣,並沒有提供原生支援。

在 Xmake 中使用依賴包

Xmake 不僅提供了內建的 xmake-repo 內建的包管理倉庫,可以直接整合使用裡面的包,還支援以相同的整合方式,去快速整合 vcpkg/conan 等第三方的依賴包。

整合一個內建依賴包只需要幾行配置:

add_requires("zlib 1.2.11")
target("test")
    add_files("src/*.c")
    add_packages("zlib")

整合一個 vcpkg 包,僅僅只需要加上對應的包管理器名稱空間,整合方式完全相同:

add_requires("vcpkg::zlib 1.2.11")
target("test")
    add_files("src/*.c")
    add_packages("vcpkg::zlib")

整合一個 conan 包,或者 conda, homebrew, pacman, apt, clib 等第三方包,也只需要改成 conan::zlib 就行了,使用者可以隨意切換包源。

另外,Xmake 會自動幫你呼叫 vcpkg/conan install 安裝命令去安裝依賴包,然後整合它們,不需要使用者做任何其他事情,僅僅只需要執行 xmake 一鍵編譯。

Xmake 和 C/C++ 包管理

C/C++ 包太少?

覺得 Xmake 內建的包倉庫裡面的包太少麼?完全沒關係,理論上,你可以通過 Xmake 使用整個 C/C++ 生態 90% 的常用依賴包,就是因為 Xmake 可以快速從各種其他包管理器中整合包來使用。

目前 Xmake 支援的包源有以下這些:

  • Official package repository xmake-repo (tbox >1.6.1)
  • Official package manager Xrepo
  • User-built repositories
  • Conan (conan::openssl/1.1.1g)
  • Conda (conda::libpng 1.3.67)
  • Vcpkg (vcpkg:ffmpeg)
  • Homebrew/Linuxbrew (brew::pcre2/libpcre2-8)
  • Pacman on archlinux/msys2 (pacman::libcurl)
  • Apt on ubuntu/debian (apt::zlib1g-dev)
  • Clib (clib::clibs/bytes@0.0.4)
  • Dub (dub::log 0.4.3)
  • Portage on Gentoo/Linux (portage::libhandy)
  • Nimble for nimlang (nimble::zip >1.3)
  • Cargo for rust (cargo::base64 0.13.0)

基本上,這些倉庫基本已經覆蓋了 C/C++ 使用者日常所需的所有包。

作者從寫這篇文章開始,統計了下 vcpkg/conan/xmake-repo 倉庫的包數量:

  • vcpkg: 1859
  • conan: 1218
  • xmake-repo: 651

可以看到,目前 Xmake 內建倉庫的包數量,已經快要接近 vcpkg/conan 了,也不少了,我們也在不斷的收錄新的包進來。

但是這完全沒有關係,因為我們可以使用任意包倉庫中的包。

如果在 CMake 中使用 vcpkg,我們只能使用 1859 個包。
如果在 CMake 中使用 conan,我們只能使用 1218 個包。

而如果在 Xmake 中使用包,我們可以使用 651 (xmake-repo) + vcpkg/conan (1k+) + more (conda, homebrew, pacman, apt, clib ...) 中的包。

甚至,C/C++ 包不夠,其他語言的包也可以拿過來用,例如:Xmake 也支援從 dub/cargo 等 Dlang/Rust 的包管理器中拉取包,給 C/C++ 專案使用。

Xmake 內建包管理整合

除了接入第三方包管理,我們也更推薦優先使用整合 xmake-repo 內建倉庫中提供的包,Xmake 會提供更多特性支援。

因此,如果使用者需要的包還沒被收錄,可以先嚐試提交到 xmake-repo 進來。

接下來,我們系統介紹下,整合內建包的一些特性。

語義版本設定

Xmake 的依賴包管理是完全支援語義版本選擇的,例如:"~1.6.1",對於語義版本的具體描述見:https://semver.org/

比如下面一些語義版本寫法:

add_requires("tbox 1.6.*", "pcre 1.3.x", "libpng ^1.18")
add_requires("libpng ~1.16", "zlib 1.1.2 || >=1.2.11 <1.3.0")

當然,如果我們對當前的依賴包的版本沒有特殊要求,那麼可以直接這麼寫:

add_requires("tbox", "libpng", "zlib")

這會使用已知的最新版本包,或者是master分支的原始碼編譯的包,如果當前包有git repo地址,我們也能指定特定分支版本:

add_requires("tbox master")
add_requires("tbox dev")

Xmake 的語義版本支援,在幾年前就已經很好的支援,而 vcpkg 也僅僅在最近一年才通過清單模式勉強支援它。

即使現在,vcpkg 對版本語義的支援也很受限,只能支援 >=1.0, 1.0 等幾種版本模式,想要選擇任意版本的包,比如 >=1.0 <1.5 等複雜版本條件的包,vcpkg 還是無法支援。

可選包設定

如果指定的依賴包當前平臺不支援,或者編譯安裝失敗了,那麼 Xmake 會編譯報錯,這對於有些必須要依賴某些包才能工作的專案,這是合理的。
但是如果有些包是可選的依賴,即使沒有也可以正常編譯使用的話,可以設定為可選包:

add_requires("tbox", {optional = true})

使用系統庫

預設的設定,Xmake 會去優先檢測系統庫是否存在(如果沒設定版本要求),如果使用者完全不想使用系統庫以及第三方包管理提供的庫,那麼可以設定:

add_requires("tbox", {system = false})

而如果配置成:

add_requires("tbox", {system = true})

就是僅僅查詢使用系統庫,不會去遠端下載安裝它,這類似於 CMake 的 find_package,但是整合方式更加簡單一致。

使用除錯版本的包

如果我們想同時原始碼除錯依賴包,那麼可以設定為使用debug版本的包(當然前提是這個包支援debug編譯):

add_requires("tbox", {debug = true})

啟用包的可選特性

我們也可以安裝帶有指定特性的包,比如安裝開啟了 zlib 和 libx265 的 ffmpeg 包。

add_requires("ffmpeg", {configs = {zlib = true, libx265 = true}})

傳遞額外的編譯選項

我們也可以傳遞額外的編譯選項給包:

add_requires("spdlog", {configs = {cxflags = "-Dxxx"}})

獨立的包管理命令 Xrepo

Xrepo 是一個基於 Xmake 的跨平臺 C/C++ 包管理器。

它是一個獨立於 Xmake 的命令程式,用於輔助使用者去管理依賴包,類似 vcpkg/conan,但相比它們,有額外多了一些實用的特性,我們會簡單介紹一些。

多倉庫管理

除了可以直接從官方倉庫:xmake-repo 檢索安裝包之外,
我們還可以新增任意多個自建的倉庫,甚至可以完全隔離外網,僅僅在公司內部網路維護私有包的安裝整合。

只需要通過下面的命令,新增上自己的倉庫地址:

$ xrepo add-repo myrepo https://github.com/mygroup/myrepo

基本使用

$ xrepo install zlib tbox

安裝指定版本包

完整支援 Semantic Versioning (語義版本)。

$ xrepo install "zlib 1.2.x"
$ xrepo install "zlib >=1.2.0"

安裝指定平臺包

$ xrepo install -p iphoneos -a arm64 zlib
$ xrepo install -p android [--ndk=/xxx] zlib
$ xrepo install -p mingw [--mingw=/xxx] zlib
$ xrepo install -p cross --sdk=/xxx/arm-linux-musleabi-cross zlib

安裝除錯版本包

$ xrepo install -m debug zlib

安裝動態庫版本包

$ xrepo install -k shared zlib

安裝指定配置包

$ xrepo install -f "vs_runtime=MD" zlib
$ xrepo install -f "regex=true,thread=true" boost

安裝第三方包管理器的包

$ xrepo install brew::zlib
$ xrepo install vcpkg::zlib
$ xrepo install conan::zlib/1.2.11

檢視包的庫使用資訊

$ xrepo fetch pcre2
{
  {
    linkdirs = {
      "/usr/local/Cellar/pcre2/10.33/lib"
    },
    links = {
      "pcre2-8"
    },
    defines = {
      "PCRE2_CODE_UNIT_WIDTH=8"
    },
    includedirs = "/usr/local/Cellar/pcre2/10.33/include"
  }
}
$ xrepo fetch --ldflags openssl
-L/Users/ruki/.xmake/packages/o/openssl/1.1.1/d639b7d6e3244216b403b39df5101abf/lib -lcrypto -lssl
$ xrepo fetch --cflags openssl
-I/Users/ruki/.xmake/packages/o/openssl/1.1.1/d639b7d6e3244216b403b39df5101abf/include
$ xrepo fetch -p [iphoneos|android] --cflags "zlib 1.2.x"
-I/Users/ruki/.xmake/packages/z/zlib/1.2.11/df72d410e7e14391b1a4375d868a240c/include
$ xrepo fetch --cflags --ldflags conan::zlib/1.2.11
-I/Users/ruki/.conan/data/zlib/1.2.11/_/_/package/f74366f76f700cc6e991285892ad7a23c30e6d47/include -L/Users/ruki/.conan/data/zlib/1.2.11/_/_/package/f74366f76f700cc6e991285892ad7a23c30e6d47/lib -lz

匯入匯出安裝後的包

xrepo 可以快速匯出已經安裝後的包,包括對應的庫檔案,標頭檔案等等。

$ xrepo export -o /tmp/output zlib

也可以在其他機器上匯入之前匯出的安裝包,實現包的遷移。

$ xrepo import -i /xxx/packagedir zlib

搜尋支援的包

$ xrepo search zlib "pcr*"
    zlib:
      -> zlib: A Massively Spiffy Yet Delicately Unobtrusive Compression Library (in xmake-repo)
    pcr*:
      -> pcre2: A Perl Compatible Regular Expressions Library (in xmake-repo)
      -> pcre: A Perl Compatible Regular Expressions Library (in xmake-repo)

另外,現在還可以從 vcpkg, conan, conda 以及 apt 等第三方包管理器中搜尋它們的包,只需要加上對應的包名稱空間就行,例如:

$ xrepo search vcpkg::pcre
The package names:
    vcpkg::pcre:
      -> vcpkg::pcre-8.44#8: Perl Compatible Regular Expressions
      -> vcpkg::pcre2-10.35#2: PCRE2 is a re-working of the original Perl Compatible Regular Expressions library
$ xrepo search conan::openssl
The package names:
    conan::openssl:
      -> conan::openssl/1.1.1g:
      -> conan::openssl/1.1.1h:

包虛擬環境管理

我們可以通過在當前目錄下,新增 xmake.lua 檔案,定製化一些包配置,然後進入特定的包 shell 環境。

add_requires("zlib 1.2.11")
add_requires("python 3.x", "luajit")
$ xrepo env shell
> python --version
> luajit --version

在 Xmake 中整合第三方構建系統

在 Xmake 中整合 Cmake 專案

Xmake 並不打算分裂 C/C++ 生態,它能很好和相容複用現有 cmake/autoconf/meson 維護的專案,比如可以將一些其他使用 CMake 維護的程式碼庫,直接本地整合進來,參與混合編譯。

也就是說,Xmake 不會強制使用者將所有的專案重新 port 到 xmake.lua,現有的 CMake 專案,一樣可以快速整合到 Xmake 專案中去。

例如,我們有如下專案結構:

├── foo
│   ├── CMakeLists.txt
│   └── src
│       ├── foo.c
│       └── foo.h
├── src
│   └── main.c
├── test.lua
└── xmake.lua

foo 目錄下是一個使用 CMake 維護的靜態庫,而根目錄下使用了 Xmake 來維護,我們可以在 xmake.lua 中通過定義 package("foo") 包來描述如何構建 foo 程式碼庫。

add_rules("mode.debug", "mode.release")

package("foo")
    add_deps("cmake")
    set_sourcedir(path.join(os.scriptdir(), "foo"))
    on_install(function (package)
        local configs = {}
        table.insert(configs, "-DCMAKE_BUILD_TYPE=" .. (package:debug() and "Debug" or "Release"))
        table.insert(configs, "-DBUILD_SHARED_LIBS=" .. (package:config("shared") and "ON" or "OFF"))
        import("package.tools.cmake").install(package, configs)
    end)
    on_test(function (package)
        assert(package:has_cfuncs("add", {includes = "foo.h"}))
    end)
package_end()

add_requires("foo")

target("demo")
    set_kind("binary")
    add_files("src/main.c")
    add_packages("foo")

其中,我們通過 set_sourcedir() 來設定 foo 包的程式碼目錄位置,然後通過 import 匯入 package.tools.cmake 輔助模組來呼叫 cmake 構建程式碼,xmake 會自動獲取生成的 libfoo.a 和對應的標頭檔案。

!> 如果僅僅本地原始碼整合,我們不需要額外設定 add_urlsadd_versions

關於包的配置描述,詳情見:包描述說明

定義完包後,我們就可以通過 add_requires("foo")add_packages("foo") 來整合使用它了,就跟整合遠端包一樣的使用方式。

另外,on_test 是可選的,如果想要嚴格檢測包的編譯安裝是否成功,可以在裡面做一些測試。

完整例子見:Library with CMakeLists

在 Xmake 中整合 Meson 專案

Xmake 支援整合更多其他構建系統維護的第三方原始碼庫,比如 Meson,僅僅只需要匯入使用 package.tools.meson 輔助構建模組呼叫 meson 來構建它們。

例如,我們從 xmake-repo 倉庫中挑選一個使用 meson 構建的包作為例子:

package("harfbuzz")
    set_sourcedir(path.join(os.scriptdir(), "3rd/harfbuzz"))
    add_deps("meson")
    on_install(function (package)
        local configs = {"-Dtests=disabled", "-Ddocs=disabled", "-Dbenchmark=disabled", "-Dcairo=disabled", "-Dfontconfig=disabled", "-Dglib=disabled", "-Dgobject=disabled"}
        table.insert(configs, "-Ddefault_library=" .. (package:config("shared") and "shared" or "static"))
        import("package.tools.meson").install(package, configs)
    end)

在 Xmake 中整合 Autoconf 專案

我們也可以使用 package.tools.autoconf 來本地整合帶有 autoconf 維護的第三方程式碼庫。

package("libev")
    set_sourcedir(path.join(os.scriptdir(), "3rd/libev"))
    on_install(function (package)
        import("package.tools.autoconf").install(package)
    end)

package.tools.autoconfpackage.tools.cmake 模組都是可以支援 mingw/cross/iphoneos/android 等交叉編譯平臺和工具鏈的,xmake 會自動傳遞對應的工具鏈進去,使用者不需要做任何其他事情。

在 Xmake 中整合 Gn 專案

我們也可以使用 package.tools.gn 來本地整合帶有 GN 維護的第三方程式碼庫。

package("skia")
    set_sourcedir(path.join(os.scriptdir(), "3rd/skia"))
    add_deps("gn", "ninja")
    on_install(function (package)
        import("package.tools.gn").install(package)
    end)

這裡有完整的指令碼例子:Skia with GN

在 Xmake 中查詢使用 CMake/C++ 包

現在 CMake 已經是事實上的標準,所以 CMake 提供的 find_package 已經可以查詢大量的系統庫和模組,我們也可以完全複用 CMake 的這部分生態來擴充 xmake 對包的整合。

只需要像整合 vcpkg/conan 包那樣,將包名稱空間改成 cmake:: 就可以了。

add_requires("cmake::ZLIB", {alias = "zlib", system = true})
target("test")
    set_kind("binary")
    add_files("src/*.c")
    add_packages("zlib")

我們指定 system = true 告訴 xmake 強制從系統中呼叫 cmake 查詢包,如果找不到,不再走安裝邏輯,因為 cmake 沒有提供類似 vcpkg/conan 等包管理器的安裝功能,只提供了包查詢特性。

指定版本

add_requires("cmake::OpenCV 4.1.1", {system = true})

指定元件

add_requires("cmake::Boost", {system = true, configs = {components = {"regex", "system"}})}

預設開關

add_requires("cmake::Boost", {system = true, configs = {components = {"regex", "system"},
                                             presets = {Boost_USE_STATIC_LIB = true}}})

相當於內部呼叫 find_package 查詢包之前,在 CMakeLists.txt 中預定義一些配置,控制 find_package 的查詢策略和狀態。

set(Boost_USE_STATIC_LIB ON) -- will be used in FindBoost.cmake
find_package(Boost REQUIRED COMPONENTS regex system)

設定環境變數

add_requires("cmake::OpenCV", {system = true, configs = {envs = {CMAKE_PREFIX_PATH = "xxx"}}})

指定自定義 FindFoo.cmake 模組指令碼目錄

mydir/cmake_modules/FindFoo.cmake

add_requires("cmake::Foo", {system = true, configs = {moduledirs = "mydir/cmake_modules"}})

在 Cmake 中整合 Xrepo 依賴包

除了可以在 Xmake 中整合 CMake 專案,我們也可以在 CMake 中直接整合 Xmake/Xrepo 提供的包,只需要使用 xrepo-cmake 提供的 CMake Wrapper。

例如:

cmake_minimum_required(VERSION 3.13.0)
project(foo)

# Download xrepo.cmake if not exists in build directory.
if(NOT EXISTS "${CMAKE_BINARY_DIR}/xrepo.cmake")
    message(STATUS "Downloading xrepo.cmake from https://github.com/xmake-io/xrepo-cmake/")
    # mirror https://cdn.jsdelivr.net/gh/xmake-io/xrepo-cmake@main/xrepo.cmake
    file(DOWNLOAD "https://raw.githubusercontent.com/xmake-io/xrepo-cmake/main/xrepo.cmake"
                  "${CMAKE_BINARY_DIR}/xrepo.cmake"
                  TLS_VERIFY ON)
endif()

# Include xrepo.cmake so we can use xrepo_package function.
include(${CMAKE_BINARY_DIR}/xrepo.cmake)

xrepo_package("zlib")

add_executable(example-bin "")
target_sources(example-bin PRIVATE
    src/main.cpp
)
xrepo_target_packages(example-bin zlib)

新增帶有配置的包

我們,也可以跟在 Xmake 中一樣,定製包的可選特性。

xrepo_package("gflags 2.2.2" CONFIGS "shared=true,mt=true")

add_executable(example-bin "")
target_sources(example-bin PRIVATE
    src/main.cpp
)
xrepo_target_packages(example-bin gflags)

使用來自第三個儲存庫的包

除了從 Xmake 官方維護的儲存庫安裝軟體包之外,我們也可以直接在 CMake 中使用它來安裝來自第三方倉庫的包,只需將倉庫名稱新增為名稱空間即可。

例如:vcpkg::zlib, conan::pcre2

xrepo_package("conan::gflags/2.2.2")
xrepo_package("conda::gflags 2.2.2")
xrepo_package("vcpkg::gflags")
xrepo_package("brew::gflags")

通過這種方式,我們將在 CMake 中整合使用 vcpkg/conan 包的方式進行了統一,並且額外提供了自動包安裝特性,以及對 homebrew/conda 等其他包倉庫的支援。

相關文章