Gopher的Rust第一課:Rust的依賴管理
共 46698字,需瀏覽 94分鐘
·
2024-06-21 11:09
在上一章《Gopher的Rust第一課:Rust代碼組織》中,我們了解了Rust的代碼組織形式,知道了基于Cargo構(gòu)建項(xiàng)目以及Rust代碼組織是目前的標(biāo)準(zhǔn)方式,同時(shí)Cargo也是管理項(xiàng)目外部依賴的標(biāo)準(zhǔn)方法,而項(xiàng)目內(nèi)部的代碼組織則由Rust module來完成。
在這一章中,我們將聚焦Rust的依賴管理,即Cargo對外部crate依賴的管理操作。我將先介紹幾種依賴的來源類型(來自crates.io或其他Package Registries、來自某個(gè)git倉庫以及來自本地的crate等),然后說說Cargo依賴的常見操作,包括依賴的添加、升降版本和刪除;最后,聊一下如何處理依賴同一個(gè)依賴項(xiàng)的不同版本。
作為Gopher,我們先來簡略回顧一下Go的依賴管理要點(diǎn),大家可以在學(xué)習(xí)Cargo依賴管理后自己做個(gè)簡單的對比,看看各自的優(yōu)缺點(diǎn)是什么。
5.1 Go依賴管理回顧
從Go 1.11版本[2]開始,Go引入了Go Modules[3]以替代舊的GOPATH方式進(jìn)行依賴管理。
我們可以使用go mod init命令初始化一個(gè)新的Go模塊。go mod init會(huì)創(chuàng)建一個(gè)go.mod文件,該文件記錄了當(dāng)前項(xiàng)目的模塊路徑,并通過require directive記錄了當(dāng)前模塊的依賴項(xiàng)以及版本:
require github.com/some/module v1.2.3
在開發(fā)過程中,我們也可以使用replace替換某個(gè)模塊的路徑,例如將依賴指向本地代碼庫進(jìn)行調(diào)試:
replace example.com/some/module => ../local/module
或是通過replace將依賴指向某個(gè)特定版本的包。Go 1.18[4]引入的Go工作區(qū)模式[5]讓依賴本地包的動(dòng)作更為便利絲滑。
Go Modules支持語義版本控制(semver),版本號格式為vX.Y.Z(其中X是major,Y為minor,Z為patch)。當(dāng)發(fā)生不兼容變化時(shí)X編號需要+1。Go創(chuàng)新性地使用了語義版本導(dǎo)入機(jī)制,通過在包導(dǎo)入路徑上使用vX來支持導(dǎo)入同一個(gè)包的不同major版本:
import (
"github.com/some/module"
v2 "github.com/some/module/v2"
)
無論是Go代碼中引入新依賴,還是通過go mod edit命令手工修改依賴(升級、更新版本或降級版本),通過go mod tidy這個(gè)萬能命令都可以自動(dòng)清理和整理依賴。go module還支持使用go.sum文件來記錄每個(gè)依賴項(xiàng)的精確版本和校驗(yàn)和,確保依賴的完整性和安全性。go.sum文件應(yīng)當(dāng)提交到版本控制系統(tǒng)中。
此外,go mod vendor支持將依賴項(xiàng)副本存儲(chǔ)在本地,這可以使你的項(xiàng)目在沒有網(wǎng)絡(luò)連接的情況下構(gòu)建,并且可以避免依賴項(xiàng)版本沖突。
Go并沒有采用像Rust、Js那樣的中心module registry,而是采用了分布式go proxy來實(shí)現(xiàn)依賴發(fā)現(xiàn)與獲取,默認(rèn)的goproxy為proxy.golang.org,國內(nèi)Gopher可以使用goproxy.cn、goproxy.io以及幾個(gè)大廠提供的GOPROXY。
注:更多關(guān)于Go module依賴管理的系統(tǒng)且詳細(xì)的內(nèi)容,可以看看我在極客時(shí)間“Go語言第一課”專欄[6]中的兩講:06|構(gòu)建模式:Go是怎么解決包依賴管理問題的?[7]和07|構(gòu)建模式:Go Module的6類常規(guī)操作[8]。
接下來,我們正式進(jìn)入Rust的依賴管理環(huán)節(jié),我們先來看看Cargo依賴的來源。
5.2 Cargo依賴的來源
Rust的依賴管理系統(tǒng)中,Rust項(xiàng)目主要有以下幾種依賴來源:
-
來自crates.io的依賴:這是Rust官方的crate registry,包含了大量開源的Rust庫。 -
來自某個(gè)git倉庫的依賴:可以從任何git倉庫添加依賴,特別是在開發(fā)階段或使用未發(fā)布的版本時(shí)非常有用。 -
來自本地的crate依賴:可以添加本地文件系統(tǒng)中的crate,便于在開發(fā)過程中引用本地代碼。
接下來,我們就來逐一看看在一個(gè)Cargo項(xiàng)目中如何配置這三種不同來源的依賴。
5.2.1 來自crates.io的依賴
在Rust中,最常見的依賴來源是crates.io,這也是Rust官方維護(hù)的中心crate registry,我們可以通過cargo命令或手工修改Cargo.toml文件來添加這些依賴。我們用一個(gè)示例來說明一下如何為當(dāng)前項(xiàng)目添加來自crates.io的依賴。
我們先用cargo創(chuàng)建一個(gè)名為hello_world的binary項(xiàng)目:
$cargo new hello_world --bin
Created binary (application) `hello_world` package
$cat Cargo.toml
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
//managing-deps/hello_world/src/main.rs
fn main() {
println!("Hello, world!");
}
構(gòu)建該項(xiàng)目,這與我們在《Gopher的Rust第一課:第一個(gè)Rust程序[9]》一文中描述的別無二致:
$cargo build
Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 1.07s
$./target/debug/hello_world
Hello, world!
現(xiàn)在我們改造一下main.rs代碼,添加點(diǎn)“實(shí)用”代碼(改自serde的example):
//managing-deps/hello_world/src/main.rs
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct Point {
x: i32,
y: i32,
}
fn main() {
println!("Hello, world!");
let point = Point { x: 1, y: 2 };
// Convert the Point to a JSON string.
let serialized = serde_json::to_string(&point).unwrap();
// Prints serialized = {"x":1,"y":2}
println!("serialized = {}", serialized);
// Convert the JSON string back to a Point.
let deserialized: Point = serde_json::from_str(&serialized).unwrap();
// Prints deserialized = Point { x: 1, y: 2 }
println!("deserialized = {:?}", deserialized);
}
然后我們通過cargo check命令檢查一下源碼是否可以編譯通過:
$cargo check
Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
error[E0432]: unresolved import `serde`
--> src/main.rs:1:5
|
1 | use serde::{Deserialize, Serialize};
| ^^^^^ use of undeclared crate or module `serde`
error[E0433]: failed to resolve: use of undeclared crate or module `serde_json`
--> src/main.rs:14:22
|
14 | let serialized = serde_json::to_string(&point).unwrap();
| ^^^^^^^^^^ use of undeclared crate or module `serde_json`
error[E0433]: failed to resolve: use of undeclared crate or module `serde_json`
--> src/main.rs:20:31
|
20 | let deserialized: Point = serde_json::from_str(&serialized).unwrap();
| ^^^^^^^^^^ use of undeclared crate or module `serde_json`
Some errors have detailed explanations: E0432, E0433.
For more information about an error, try `rustc --explain E0432`.
error: could not compile `hello_world` (bin "hello_world") due to 3 previous errors
cargo check提示找不到serde、serde_json兩個(gè)crate。并且,cargo check執(zhí)行后,多出一個(gè)Cargo.lock文件。由于此時(shí)尚未在Cargo.toml中添加依賴(雖然代碼中明確了對serde和serde_json的依賴),Cargo.lock中還沒有依賴package的具體信息:
$cat Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "hello_world"
version = "0.1.0"
Rust是否可以像go module那樣通過go mod tidy自動(dòng)掃描源碼并在Cargo.toml中補(bǔ)全依賴信息呢?然而并沒有。Rust添加依賴的操作還是需要手動(dòng)完成。
我們的rust源碼依賴serde和serde_json,接下來,我們就需要在Cargo.toml中手工添加serde、serde_json依賴,當(dāng)然最標(biāo)準(zhǔn)的方法還是通過cargo add命令:
$cargo add serde serde_json
Adding serde v1.0.202 to dependencies.
Features:
+ std
- alloc
- derive
- rc
- serde_derive
- unstable
Adding serde_json v1.0.117 to dependencies.
Features:
+ std
- alloc
- arbitrary_precision
- float_roundtrip
- indexmap
- preserve_order
- raw_value
- unbounded_depth
我們查看一下cargo add執(zhí)行后的Cargo.toml:
$cat Cargo.toml
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = "1.0.202"
serde_json = "1.0.117"
我們看到在dependencies下新增了兩個(gè)直接依賴信息:serde和serde_json以及它們的版本信息。
關(guān)于依賴版本,Cargo定義了的兼容性規(guī)則[10]如下:
針對在1.0版本之前的版本,比如0.x.y,語義版本規(guī)范[11]認(rèn)為是處于初始開發(fā)階段,公共API是不穩(wěn)定的,因此沒有明確兼容性語義。但Cargo對待這樣的版本的規(guī)則是:0.x.y與0.x.z是兼容的,如果x > 0且y >=z。比如:0.1.10是兼容0.1.1的。而在1.0版本之后,Cargo參考語義版本規(guī)范確定版本兼容性。
基于上述的兼容性規(guī)則,在Cargo.toml中指定依賴版本的形式與語義有如下幾種情況:
some_crate = "1.2.3" => 版本范圍[1.2.3, 2.0.0)。
some_crate = "1.2" => 版本范圍[1.2.0, 2.0.0)。
some_crate = "1" => 版本范圍[1.0.0, 2.0.0)。
some_crate = "0.2.3" => 版本范圍[0.2.3, 0.3.0)。
some_crate = "0.2" => 版本范圍[0.2.0, 0.3.0)。
some_crate = "0" => 版本范圍[0.0.0, 1.0.0)。
some_crate = "0.0" => 版本范圍[0.0.0, 0.1.0)。
some_crate = "0.0.3" => 版本范圍[0.0.3, 0.0.4)。
some_crate = "^1.2.3" => 版本范圍[1.2.3]。
some_crate = "~1.2.3" => 版本范圍[1.2.3, 1.3.0)。
some_crate = "~1.2" => 版本范圍[1.2.0, 1.3.0)。
some_crate = "~1" => 版本范圍[1.0.0, 2.0.0)。
Cargo還支持一些帶有通配符的版本需求形式:
some_crate = "*" => 版本范圍[0.0.0, )。
some_crate = "1.*" => 版本范圍[1.0.0, 2.0.0)。
some_crate = "1.2.*" => 版本范圍[1.2.0, 1.3.0)。
如果要限制最高版本范圍,可以用帶有多版本的需求形式:
some_crate = ">=1.2, < 1.5" => 版本范圍[1.2.0, 1.5.0)。
有了版本范圍后,Cargo初始就會(huì)使用該范圍內(nèi)的當(dāng)前最大版本號版本作為依賴的最終版本。比如some_crate = "1.2.3",但當(dāng)前some_crate的最高版本為1.3.5,那么Cargo會(huì)選擇1.3.5的some_crate作為當(dāng)前項(xiàng)目的依賴。
如果一個(gè)項(xiàng)目有兩個(gè)依賴項(xiàng)同時(shí)依賴另外一個(gè)共同的依賴,比如(例子來自Cargo book):
# Package A
[dependencies]
bitflags = "1.0"
# Package B
[dependencies]
bitflags = "1.1"
那么A依賴bitflags的范圍在[1.0.0, 2.0.0),B依賴bitflags的范圍在[1.1.0, 2.0.0),這樣如果當(dāng)前bitflags的最新版本為1.2.1,那么Cargo會(huì)選擇1.2.1作為bitflags的最終版本。這點(diǎn)與Go的最小版本選擇(mvs)[12]是不一樣的,在這個(gè)示例情況下,Go會(huì)選擇bitflags的1.1.0版本,即滿足A和B的bitflags的最小版本即可。
后續(xù)當(dāng)依賴的版本有更新時(shí),可以執(zhí)行cargo update升級依賴的版本到一個(gè)兼容的、更高的版本(體現(xiàn)在Cargo.lock文件中依賴的版本更新)。
Cargo.lock是鎖定Cargo最終采用的依賴的版本的描述文件,這個(gè)文件由cargo管理,不要手動(dòng)修改,這時(shí)的Cargo.lock文件如下:
$cat Cargo.lock
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "hello_world"
version = "0.1.0"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "proc-macro2"
version = "1.0.83"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b33eb56c327dec362a9e55b3ad14f9d2f0904fb5a5b03b513ab5465399e9f43"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2863d96a84c6439701d7a38f9de935ec562c8832cc55d1dde0f513b52fad106"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
和go.sum類似(但go.sum并不指示依賴項(xiàng)采用的具體版本), Cargo.lock中對于每個(gè)依賴項(xiàng)都包括名字、具體某個(gè)版本、來源與校驗(yàn)和。
我們再用cargo check一下該項(xiàng)目是否可以編譯成功:
$cargo check
Compiling serde v1.0.202
Compiling serde_json v1.0.117
Checking ryu v1.0.18
Checking itoa v1.0.11
Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
error: cannot find derive macro `Serialize` in this scope
--> src/main.rs:3:10
|
3 | #[derive(Serialize, Deserialize, Debug)]
| ^^^^^^^^^
|
note: `Serialize` is imported here, but it is only a trait, without a derive macro
--> src/main.rs:1:26
|
1 | use serde::{Deserialize, Serialize};
| ^^^^^^^^^
error: cannot find derive macro `Deserialize` in this scope
--> src/main.rs:3:21
|
3 | #[derive(Serialize, Deserialize, Debug)]
| ^^^^^^^^^^^
|
note: `Deserialize` is imported here, but it is only a trait, without a derive macro
--> src/main.rs:1:13
|
1 | use serde::{Deserialize, Serialize};
| ^^^^^^^^^^^
error[E0277]: the trait bound `Point: Serialize` is not satisfied
--> src/main.rs:14:44
|
14 | let serialized = serde_json::to_string(&point).unwrap();
| --------------------- ^^^^^^ the trait `Serialize` is not implemented for `Point`
| |
| required by a bound introduced by this call
|
= help: the following other types implement trait `Serialize`:
bool
char
isize
i8
i16
i32
i64
i128
and 131 others
note: required by a bound in `serde_json::to_string`
--> /Users/tonybai/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/serde_json-1.0.117/src/ser.rs:2209:17
|
2207 | pub fn to_string<T>(value: &T) -> Result<String>
| --------- required by a bound in this function
2208 | where
2209 | T: ?Sized + Serialize,
| ^^^^^^^^^ required by this bound in `to_string`
error[E0277]: the trait bound `Point: Deserialize<'_>` is not satisfied
--> src/main.rs:20:31
|
20 | let deserialized: Point = serde_json::from_str(&serialized).unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `Deserialize<'_>` is not implemented for `Point`
|
= help: the following other types implement trait `Deserialize<'de>`:
bool
char
isize
i8
i16
i32
i64
i128
and 142 others
note: required by a bound in `serde_json::from_str`
--> /Users/tonybai/.cargo/registry/src/rsproxy.cn-8f6827c7555bfaf8/serde_json-1.0.117/src/de.rs:2676:8
|
2674 | pub fn from_str<'a, T>(s: &'a str) -> Result<T>
| -------- required by a bound in this function
2675 | where
2676 | T: de::Deserialize<'a>,
| ^^^^^^^^^^^^^^^^^^^ required by this bound in `from_str`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `hello_world` (bin "hello_world") due to 4 previous errors
似乎是依賴包缺少某個(gè)feature。我們重新add一下serde依賴,這次帶著必要的feature:
$cargo add serde --features derive,serde_derive
Adding serde v1.0.202 to dependencies.
Features:
+ derive
+ serde_derive
+ std
- alloc
- rc
- unstable
然后再執(zhí)行check:
$cargo check
Compiling proc-macro2 v1.0.83
Compiling unicode-ident v1.0.12
Compiling serde v1.0.202
Compiling quote v1.0.36
Compiling syn v2.0.65
Compiling serde_derive v1.0.202
Checking serde_json v1.0.117
Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 8.50s
我們看到,當(dāng)開啟serde的derive和serde_derive feature后,項(xiàng)目代碼就可以正常編譯和運(yùn)行了,下面是運(yùn)行結(jié)果:
$cargo run
Compiling itoa v1.0.11
Compiling ryu v1.0.18
Compiling serde v1.0.202
Compiling serde_json v1.0.117
Compiling hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
Finished dev [unoptimized + debuginfo] target(s) in 4.16s
Running `target/debug/hello_world`
Hello, world!
serialized = {"x":1,"y":2}
deserialized = Point { x: 1, y: 2 }
注:feature是cargo提供的一種條件編譯和選項(xiàng)依賴的機(jī)制,有些類似于Go build constraints[13],但表達(dá)能力和控制精細(xì)度要遠(yuǎn)超go build constraints,但其復(fù)雜度也遠(yuǎn)超go build constraints。在本章中,我們不對feature進(jìn)行展開說明,更多關(guān)于feature的詳細(xì)說明,請參見cargo feature參考手冊[14]。
除了官方的crates.io,Cargo還支持來自其他非官方的Registry的依賴,比如使用企業(yè)私有crate registry,這個(gè)不在本章內(nèi)容范圍內(nèi),后續(xù)會(huì)考慮用專題的形式說明。
考慮crates.io在海外,國內(nèi)Rustaceans可以考慮使用國內(nèi)的crate源[15],比如使用rsproxy源的配置如下:
// ~/.cargo/config
[source.crates-io]
replace-with = 'rsproxy'
[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"
[net]
git-fetch-with-cli = true
git-fetch-with-cli = true表示使用本地git命令去獲取registry index,否則使用內(nèi)置的git庫來獲取。
5.2.2 來自git倉庫的依賴
有時(shí)候,我們可能需要依賴一個(gè)尚未發(fā)布到crates.io上的庫,這時(shí)可以通過git倉庫來添加依賴。當(dāng)然,這一方式也非常適合一些企業(yè)內(nèi)的私有g(shù)it倉庫上的依賴。在Go中,如果沒有一些額外的IT設(shè)置支持,便很難拉取私有倉庫上的go module[16]。
下面我們使用下面命令將Cargo.toml中的serde依賴改為從git repo獲取:
$cargo add serde --features derive,serde_derive --git https://github.com/serde-rs/serde.git
Updating git repository `https://github.com/serde-rs/serde.git`
Adding serde (git) to dependencies.
Features:
+ derive
+ serde_derive
+ std
- alloc
- rc
- unstable
更新后的Cargo.toml依賴列表變?yōu)榱耍?/p>
[dependencies]
serde = { git = "https://github.com/serde-rs/serde.git", version = "1.0.202", features = ["derive", "serde_derive"] }
serde_json = "1.0.117"
不過當(dāng)我執(zhí)行cargo check時(shí)報(bào)如下錯(cuò)誤:
$cargo check
Updating git repository `https://github.com/serde-rs/serde.git`
remote: Enumerating objects: 28491, done.
remote: Counting objects: 100% (6879/6879), done.
remote: Compressing objects: 100% (763/763), done.
remote: Total 28491 (delta 6255), reused 6560 (delta 6111), pack-reused 21612
Receiving objects: 100% (28491/28491), 7.97 MiB | 205.00 KiB/s, done.
Resolving deltas: 100% (20065/20065), done.
From https://github.com/serde-rs/serde
* [new ref] -> origin/HEAD
* [new tag] v0.2.0 -> v0.2.0
* [new tag] v0.2.1 -> v0.2.1
* [new tag] v0.3.0 -> v0.3.0
* [new tag] v0.3.1 -> v0.3.1
... ...
* [new tag] v1.0.98 -> v1.0.98
* [new tag] v1.0.99 -> v1.0.99
Compiling serde v1.0.202
Compiling serde_derive v1.0.202 (https://github.com/serde-rs/serde.git#37618545)
Compiling serde v1.0.202 (https://github.com/serde-rs/serde.git#37618545)
Checking serde_json v1.0.117
Checking hello_world v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/hello_world)
error[E0277]: the trait bound `Point: serde::ser::Serialize` is not satisfied
--> src/main.rs:14:44
... ...
在serde的github issue中,這個(gè)問題似乎已經(jīng)修正[17],但在我的環(huán)境下不知何故依舊存在。
在使用git來源時(shí),我們也可以指定一個(gè)特定的分支、tag或者commit:
[dependencies]
serde = { git = "https://github.com/serde-rs/serde.git", branch = "next" }
# 或者
serde = { git = "https://github.com/serde-rs/serde.git", tag = "v1.0.104" }
# 或者
serde = { git = "https://github.com/serde-rs/serde.git", rev = "a1b2c3d4" }
5.2.3 來自本地的crate依賴
在開發(fā)過程中,我們還可能需要引用本地文件系統(tǒng)中的crate。在Go中,我們可以使用go mod的replace或者Go workspace來解決該問題。在Rust中,我們也可以通過下面方式來添加本地依賴:
$cargo add serde --features derive,serde_derive --path ../serde/serde
Adding serde (local) to dependencies.
Features:
+ derive
+ serde_derive
+ std
- alloc
- rc
- unstable
// Cargo.toml
[dependencies]
serde = { version = "1.0.202", features = ["derive", "serde_derive"], path = "../serde/serde" }
不過,和來自git一樣,基于來自本地的crate依賴,cargo check也報(bào)和基于git的crate依賴同樣的錯(cuò)誤。
5.3 Cargo依賴常見操作
下面簡要說說依賴的常見操作,以來自crates.io的依賴為例。
5.3.1 添加依賴
正如上面示例中我們演示的那樣,我們可以通過cargo add來添加一個(gè)依賴,或者可以通過手工編輯Cargo.toml文件添加對應(yīng)的配置。例如,添加一個(gè)源自crates.io的新依賴rand庫:
[dependencies]
rand = "0.8"
5.3.2 升降版本
要升級某個(gè)依賴到兼容的最新版本,可以使用cargo update;如果升級到不兼容版本,需要先修改Cargo.toml中的版本需求。例如,將rand庫升級到2.x版本:
[dependencies]
rand = "2.0"
然后運(yùn)行cargo update,Cargo會(huì)根據(jù)新的版本號需求進(jìn)行重新解析依賴。
當(dāng)然要降級依賴的版本到一個(gè)兼容的版本,通常可能需要在版本需求中使用類似“^x.y.z”來精確指定版本;如果要降級到一個(gè)不兼容版本,和升級到不兼容版本一樣,需要先修改Cargo.toml中的版本需求,然后運(yùn)行cargo update,Cargo會(huì)根據(jù)新的版本號需求進(jìn)行重新解析依賴。
5.3.3 刪除依賴
刪除一個(gè)依賴則十分容易,只需從Cargo.toml中移除或注釋掉對應(yīng)的依賴配置, 然后運(yùn)行cargo build,Cargo會(huì)更新項(xiàng)目的依賴關(guān)系。
5.4 處理依賴同一個(gè)依賴項(xiàng)的不同版本
在某些情況下,不同的crate可能依賴同一個(gè)crate的不同版本,這也是編程語言中典型的鉆石依賴問題!是一個(gè)常見的依賴管理挑戰(zhàn)。它發(fā)生在一個(gè)依賴項(xiàng)被兩個(gè)或更多其他依賴項(xiàng)共享時(shí)。比如:app依賴A、B ,而A、B又同時(shí)依賴C。
在這樣的情況下,前面我們提過Go給出的解決方案包含三點(diǎn):
-
若A、B依賴的C的版本相同,那么選取這個(gè)相同的C版本即可; -
若A、B依賴的C的版本不同但兼容(依照semver規(guī)范),那么選取C滿足A、B依賴的最小版本,這叫做最小版本選擇; -
若A、B依賴的C的版本不同且不兼容,那么通過語義導(dǎo)入版本[18],最終app將導(dǎo)入C的不同版本,這兩個(gè)版本將在app中共存。
那么在Rust項(xiàng)目中,Cargo又是如何處理的呢?我們通過一個(gè)示例分別來看看這三種情況,我們創(chuàng)建一個(gè)app的示例:
// 在rust-guide-for-gopher/managing-deps目錄下
$tree -F app
app
├── A/
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── B/
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── C/
│ ├── Cargo.lock
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── Cargo.lock
├── Cargo.toml
└── src/
└── main.rs
7 directories, 10 files
app是一個(gè)binary cargo project,它的Cargo.toml和src/main.rs內(nèi)容如下:
// app/Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
A = { path = "./A", version = "0.1.0" }
B = { path = "./B", version = "0.1.0" }
// app/src/main.rs
fn main() {
println!("Hello, world!");
A::hello_from_a();
B::hello_from_b();
}
我們看到:app依賴crate A和B,并且分別調(diào)用了兩個(gè)crate的公共函數(shù)。
接下來,我們再來看看A和B的情況,我們分場景說明。
5.4.1 依賴C的相同版本
當(dāng)A和B依賴C的相同版本時(shí),這個(gè)不難推斷cargo最終會(huì)為A和B選擇同一個(gè)依賴C的版本。比如:
$cat A/Cargo.toml
[package]
name = "A"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
C = { path = "../C", version = "1.0.0" }
$cat B/Cargo.toml
[package]
name = "B"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
C = { path = "../C", version = "1.0.0" }
$cat A/src/lib.rs
pub fn hello_from_a() {
println!("Hello from A begin");
C::hello_from_c();
println!("Hello from A end");
}
$cat B/src/lib.rs
pub fn hello_from_b() {
println!("Hello from B begin");
C::hello_from_c();
println!("Hello from B end");
}
$cat C/Cargo.toml
[package]
name = "C"
version = "1.3.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
$cat C/src/lib.rs
pub fn hello_from_c() {
println!("Hello from C 1.3.0");
}
在這里A和B對C的依賴都是version = "1.0.0",通過前面的講解我們知道,這等價(jià)于C的版本范圍為[1.0.0, 2.0.0)。而C目前的版本為1.3.0,那么Cargo就會(huì)為A和B都選擇1.3.0版本的C。我們運(yùn)行一下這個(gè)app程序:
$cargo run
... ...
Hello, world!
Hello from A begin
Hello from C 1.3.0
Hello from A end
Hello from B begin
Hello from C 1.3.0
Hello from B end
我們還可以通過cargo tree命令驗(yàn)證一下對A和B對C版本的依賴:
$cargo tree --workspace --target all --all-features --invert C
C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C)
├── A v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/A)
│ └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)
└── B v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/B)
└── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)
我們看到A和B都依賴了C的v1.3.0版本。
5.4.2 依賴C的兩個(gè)兼容版本
現(xiàn)在我們修改一下A和B對C的依賴版本需求:
$cat A/Cargo.toml
[package]
name = "A"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
C = { path = "../C", version = "1.1.1" }
$cat B/Cargo.toml
[package]
name = "B"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
C = { path = "../C", version = "1.2.3" }
讓A對C的依賴需求為1.1.1,讓B依賴需求為1.2.3,這回我們再來運(yùn)行一下cargo run和cargo tree:
$cargo run
... ...
Hello, world!
Hello from A begin
Hello from C 1.3.0
Hello from A end
Hello from B begin
Hello from C 1.3.0
Hello from B end
$cargo tree --workspace --target all --all-features --invert C
C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C)
├── A v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/A)
│ └── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)
└── B v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/B)
└── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)
由于1.1.1和1.2.3是兼容版本,因此Cargo選擇了兼容這兩個(gè)版本的C當(dāng)前的最高版本1.3.0。
5.4.3 依賴C的兩個(gè)不兼容版本
現(xiàn)在我們來試驗(yàn)一下當(dāng)A和B依賴的C版本不兼容時(shí),Cargo會(huì)為A和B選擇C的什么版本!由于是本地環(huán)境,我們無法在一個(gè)目錄下保存兩個(gè)C版本,因此我們copy一份當(dāng)前的C組件,將拷貝重命名為C-1.3.0,然后將C下面的Cargo.toml和src/lib.rs修改成下面的樣子:
$cat C/Cargo.toml
[package]
name = "C"
version = "2.4.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
$cat C/src/lib.rs
pub fn hello_from_c() {
println!("Hello from C 2.4.0");
}
然后我們修改一下A和B的依賴,讓他們分別依賴C-1.3.0和C:
$cat A/Cargo.toml
[package]
name = "A"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
C = { path = "../C-1.3.0", version = "1.1.1" }
$cat B/Cargo.toml
[package]
name = "B"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
C = { path = "../C", version = "2.2.3" }
我們再來運(yùn)行一下該app:
$cargo run
... ...
Hello, world!
Hello from A begin
Hello from C 1.3.0
Hello from A end
Hello from B begin
Hello from C 2.4.0
Hello from B end
我們看到cargo為A選擇的版本是C v1.3.0,而為B選擇的C版本是C v2.4.0,也就是說C的兩個(gè)不兼容版本在app中可以同時(shí)存在。
讓我們再來用cargo tree查看一下對C的依賴關(guān)系:
$cargo tree --workspace --target all --all-features --invert C
error: There are multiple `C` packages in your project, and the specification `C` is ambiguous.
Please re-run this command with one of the following specifications:
[email protected]
[email protected]
我們看到,cargo tree提示我們兩個(gè)版本不兼容,必須明確指明是要查看哪個(gè)C版本的依賴,那我們就分別按版本查看一下:
$cargo tree --workspace --target all --all-features --invert [email protected]
C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C-1.3.0)
└── A v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/A)
└── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)
$cargo tree --workspace --target all --all-features --invert [email protected]
C v2.4.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/C)
└── B v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app/B)
└── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/app)
5.4.4 直接依賴C的不同版本
在Go中我們可以通過語義導(dǎo)入版本實(shí)現(xiàn)在app中直接依賴同一個(gè)包的兩個(gè)不兼容版本:
import (
"github.com/user/repo"
v2 "github.com/user/repo/v2"
)
在Rust中,是否也可以實(shí)現(xiàn)這一點(diǎn)?如果可以,又是如何實(shí)現(xiàn)的呢?答案是可以。至少我們可以通過使用Cargo的依賴別名功能來實(shí)現(xiàn)。我們建立一個(gè)名為dep_alias的示例,其目錄結(jié)構(gòu)如下:
$tree -F dep_alias
dep_alias
├── C/
│ ├── Cargo.lock
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── C-1.3.0/
│ ├── Cargo.lock
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
├── Cargo.lock
├── Cargo.toml
└── src/
└── main.rs
5 directories, 9 files
在這個(gè)示例中,app依賴C-1.3.0目錄下的C 1.3.0版本以及C目錄下的C 2.4.0版本,下面是app/Cargo.toml和app/src/main.rs的代碼:
// rust-guide-for-gopher/managing-deps/dep_alias/Cargo.toml
$cat Cargo.toml
[package]
name = "app"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
C_v1 = { path = "C-1.3.0", version = "1.0.0", package = "C" }
C_v2 = { path = "C", version = "2.3.0", package = "C" }
$cat src/main.rs
$cat src/main.rs
extern crate C_v1 as C_v1;
extern crate C_v2 as C_v2;
fn main() {
C_v1::hello_from_c();
C_v2::hello_from_c();
}
這里,我們?yōu)镃的兩個(gè)不兼容版本建立了兩個(gè)別名:C_v1和C_v2,然后在代碼中分別使用C_v1和C_v2,cargo會(huì)分別為C_v1和C_v2選擇合適的版本,這里C_v1最終選擇為1.3.0,而C_v2最終定為2.4.0:
$cargo run
Hello from C 1.3.0
Hello from C 2.4.0
由于包名依然是C,所以在使用cargo tree查看依賴關(guān)系時(shí),依然要帶上不同版本:
$cargo tree --workspace --target all --all-features --invert [email protected]
C v1.3.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias/C-1.3.0)
└── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias)
$cargo tree --workspace --target all --all-features --invert [email protected]
C v2.4.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias/C)
└── app v0.1.0 (/Users/tonybai/Go/src/github.com/bigwhite/experiments/rust-guide-for-gopher/managing-deps/dep_alias)
5.5 小結(jié)
在這一章中,我們介紹了Rust中通過Cargo進(jìn)行依賴管理的基本方法。
我們首先簡要回顧了Go語言的依賴管理,特別是Go Modules的相關(guān)內(nèi)容,如go.mod文件、版本控制機(jī)制等。
接著我們介紹了Rust中通過Cargo進(jìn)行依賴管理的方法。Cargo依賴主要有三種來源:crates.io官方注冊中心、Git倉庫和本地文件系統(tǒng)。通過Cargo.toml文件和cargo命令,我們可以靈活添加、升級、降級或刪除依賴項(xiàng)。文中還講解了Cargo的版本兼容性規(guī)則和各種指定版本的語法。
針對依賴同一個(gè)庫的不同版本的情況,我通過示例說明了Cargo的處理方式:如果版本相同或兼容,Cargo會(huì)選擇滿足要求的當(dāng)前最高版本;如果版本不兼容,Cargo允許在項(xiàng)目中同時(shí)使用這些不兼容的版本,可以通過別名來區(qū)分使用。
總體來看,Cargo提供的依賴管理方式表達(dá)能力很強(qiáng)大,但相對于Go來說,還是復(fù)雜了很多,學(xué)習(xí)起來曲線要高很多,troubleshooting起來也不易,文中尚有一個(gè)遺留問題尚未解決,如果大家有解決方案或思路,可以在文章評論中告知我,感謝。
注:本文涉及的都是cargo依賴管理的基礎(chǔ)內(nèi)容,還有很多細(xì)節(jié)以及高級用法并未涉及。
本章中涉及的源碼可以在這里[19]下載。
5.6 參考資料
-
Cargo Book: Specifying Dependencies[20] - https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html -
Cargo Book: Registries[21] - https://doc.rust-lang.org/cargo/reference/registries.html
參考資料
Gopher的Rust第一課:Rust代碼組織: https://tonybai.com/2024/06/06/gopher-rust-first-lesson-organizing-rust-code
[2]Go 1.11版本: https://tonybai.com/2018/11/19/some-changes-in-go-1-11/
[3]Go Modules: https://go.dev/ref/mod
[4]Go 1.18: https://tonybai.com/2022/04/20/some-changes-in-go-1-18
[5]Go工作區(qū)模式: https://tonybai.com/2021/11/12/go-workspace-mode-in-go-1-18
[6]極客時(shí)間“Go語言第一課”專欄: http://gk.link/a/10AVZ
[7]06|構(gòu)建模式:Go是怎么解決包依賴管理問題的?: https://time.geekbang.org/column/article/429941
[8]07|構(gòu)建模式:Go Module的6類常規(guī)操作: https://time.geekbang.org/column/article/431463
[9]Gopher的Rust第一課:第一個(gè)Rust程序: https://tonybai.com/2024/05/27/gopher-rust-first-lesson-first-rust-program/
[10]Cargo定義了的兼容性規(guī)則: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
[11]語義版本規(guī)范: https://semver.org/
[12]1.0.0, 2.0.0),B依賴bitflags的范圍在[1.1.0, 2.0.0),這樣如果當(dāng)前bitflags的最新版本為1.2.1,那么Cargo會(huì)選擇1.2.1作為bitflags的最終版本。這點(diǎn)與Go的[最小版本選擇(mvs): https://research.swtch.com/vgo-mvs
[13]Go build constraints: https://pkg.go.dev/go/build#hdr-Build_Constraints
[14]cargo feature參考手冊: https://doc.rust-lang.org/cargo/reference/features.html
[15]使用國內(nèi)的crate源: https://doc.rust-lang.org/cargo/reference/source-replacement.html
[16]拉取私有倉庫上的go module: https://tonybai.com/2021/09/03/the-approach-to-go-get-private-go-module-in-house
[17]這個(gè)問題似乎已經(jīng)修正: https://github.com/serde-rs/serde/issues/2526
[18]語義導(dǎo)入版本: https://research.swtch.com/vgo-import
[19]這里: https://github.com/bigwhite/experiments/tree/master/rust-guide-for-gopher/managing-deps
[20]Cargo Book: Specifying Dependencies: https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html
[21]Cargo Book: Registries: https://doc.rust-lang.org/cargo/reference/registries.html
[22]Gopher部落知識星球: https://public.zsxq.com/groups/51284458844544
[23]鏈接地址: https://m.do.co/c/bff6eed92687
推薦閱讀:
6 個(gè)必須嘗試的將代碼轉(zhuǎn)換為引人注目的圖表的工具
Go早期是如何在Google內(nèi)部發(fā)展起來的
2024 Gopher Meetup 武漢站活動(dòng)
Go區(qū)不大,創(chuàng)造神話,科目三殺進(jìn)來了
想要了解Go更多內(nèi)容,歡迎掃描下方??關(guān)注公眾號,掃描 [實(shí)戰(zhàn)群]二維碼 ,即可進(jìn)群和我們交流~
- 掃碼即可加入實(shí)戰(zhàn)群 -
