昨天講了 vcpkg 的版本控制觀念。今天我們來講 Conan 的版本控制觀念。
D02 裡面我們提過,兩套套件管理系統的「版本觀完全相反」。今天就來比較一下,到底是怎麼樣個相反法。
Conan 每個套件都要寫版本號碼。
vcpkg 不用寫, vcpkg.json 裡只寫套件名稱,版本交給 baseline 決定。
但是 Conan 一定要寫版本,寫法有兩種:寫死一個版本號碼,或是給一個範圍。
我們之前的範例一直都是寫死的:fmt/12.2.0。但是在開發中,有時候我們想要有彈性。現代的軟體開發大多遵守語意化版本(Semantic Versioning),X.Y.Z 裡面的第一碼X跳號代表有不相容的更新,第二碼Y是新增功能且向下相容,第三碼就是修 BUG。
團隊覺得,每次只要 fmt 有了第三碼的版本號更新,我們就應該要馬上導入,這樣的要求 Conan 做得到嗎?可以,這就要靠 Conan recipe 的版本範圍。
版本範圍的寫法是用中括號包起來:
from conan import ConanFile
from conan.tools.cmake import cmake_layout
class HelloConanRecipe(ConanFile):
settings = "os", "compiler", "build_type", "arch"
generators = "CMakeDeps", "CMakeToolchain"
def requirements(self):
self.requires("fmt/[>=12.2 <12.3]")
self.requires("nlohmann_json/3.12.0")
def layout(self):
cmake_layout(self)
[>=12.2 <12.3] 中間用空白隔開,意思是「AND 兩個條件都要成立」:大於等於 12.2,而且小於 12.3。
Conan Install 安裝套件時,就會從範圍裡面挑出最新的那一個版本。
除了上面提的寫法,版本範圍還有好幾種寫法:
| 寫法 | 意思 | 實際解出來 |
|---|---|---|
fmt/[>=11 <13] |
11 以上、13 以下 | 12.2.0 |
fmt/[~12.1] |
12.1.x 前兩碼固定,只更新第三碼(修BUG) | 12.1.0 |
fmt/[^12] |
12.x 第一碼固定,更新第二三碼(相容的功能也可以更新) | 12.2.0 |
fmt/[^12.1] |
12.x 第一碼固定,更新後兩碼,但是一定要大於等於 12.1 | 12.2.0 |
fmt/[<12 || >=12.1 <12.2] |
|| OR「或」。「12 以下,或者 12.1.x」用來跳過中間的版本號碼 |
12.1.0 |
fmt/[*] |
任意版本皆可 | 12.2.0 |
這些簡寫是 npm 那邊流行起來的慣例,~ 開放第三碼、^ 開放第二三碼,剛好對應語意化版本的三個號碼。
Conan 的寫法大部分跟 npm 一樣,你可以到以下網頁玩玩版本號碼推導的各種簡寫符號:
https://jubianchi.github.io/semver-check/#/
寫了範圍之後,要怎麼快速測試 Conan 到底挑了哪個版本?
用這個指令就可以了:
conan graph info .
它會爬過整棵依賴樹,並且去解析每個版本範圍的要求,並且解析成一個最終符合的版本號碼,但不會真的去下載或編譯二進位檔。
以下是部分的輸出文字:
======== Computing dependency graph ========
Graph root
conanfile.py: /Users/matt/github/ironman-2026/D13/conanfile.py
Requirements
fmt/12.2.0#b445244e6d0151a425e229248cd2bed8 - Cache
nlohmann_json/3.12.0#2d634ab0ec8d9f56353e5ccef6d6612c - Cache
Resolved version ranges
fmt/[>=12.2 <12.3]: fmt/12.2.0
仔細看印出的文字裡有一段寫 Resolved version ranges:
這裡寫了 fmt/[>=12.2 <12.3] 解出來是最終版本是 fmt/12.2.0。
也可以不寫 conanfile,直接在命令列測試某個版本範圍:
conan graph info --requires="fmt/[~12.1]"
每個套件都自己指定版本,在稍微大型的專案,就很容易碰到一個經典問題:菱形依賴衝突(diamond dependency conflict)。
假設你的專案直接用了 fmt/12.2.0,同時也用 spdlog/1.15.3。
spdlog 內部依賴 fmt,ConanCenter 上的 spdlog 1.15.3 寫死的版本號碼是 fmt/11.2.0,跟你寫的 fmt 版本號碼不一樣:
我的專案
/ \
fmt/12.2.0 spdlog/1.15.3
\
fmt/11.2.0
兩條路徑都指向 fmt,但要求的是不同版本,這就是菱形衝突。
Conan 不會幫你解決菱形衝突,會直接報錯:
ERROR: Version conflict: Conflict between fmt/11.2.0 and fmt/12.2.0 in the graph.
Conflict originates from spdlog/1.15.3
告訴你哪兩個版本打架,衝突是從 spdlog 來的。它沒辦法自動幫你挑一個,因為換版本可能會造成 ABI 不相容。
這時候怎麼辦呢?有兩個解法。
如果你確定用 fmt/12.2.0 沒問題,可以在 conanfile.py 裡加上選項 force=True :
def requirements(self):
self.requires("fmt/12.2.0", force=True)
self.requires("spdlog/1.15.3")
force=True 的意思是:「不要管其他套件的意見了,我強制指定用 12.2.0。」
再跑一次 conan graph info .,錯誤不見了,輸出多了一段 Overrides:
Requirements
fmt/12.2.0#b445244e6d0151a425e229248cd2bed8 - Cache
spdlog/1.15.3#855569172102c04e3e0db21ae95cf017 - Cache
Overrides
fmt/11.2.0: ['fmt/12.2.0']
意思是 spdlog 原本要的 fmt/11.2.0,被換成了 fmt/12.2.0。
另一個寫法是加上選項 override=True:
def requirements(self):
self.requires("fmt/12.2.0", override=True)
self.requires("spdlog/1.15.3")
override 的不同之處在於:override 只在有衝突的時候才介入。如果依賴樹裡沒人要 fmt,override 不會把 fmt 加進來;force 則不管有沒有衝突,都會強制加入。
簡單說:override:「如果有人要這個套件,就用我指定的版本。」
不過這裡要提醒:不管是 force 還是 override,強制替換版本之後,API 和 ABI 的相容問題要自己負責處理,編譯能過不代表行為正確。
這邊放一個表格,給大家比較 Conan 和 vcpkg
| Conan | vcpkg | |
|---|---|---|
| 指定版本 | 每個套件自己指定版本 | 整批跟著 baseline |
| 版本範圍 | 有,[>=11 <13] |
只有 version>= |
| 選版方向 | 範圍內最新的版本 | 滿足條件的最舊的版本 |
| 衝突處理 | force / override |
overrides |