1.2 安裝 Rust 工具鏈(rustup、rustc、cargo)#
工具鏈的組成與分工#
初接觸 Rust 的時候,最常見的困惑是「到底要裝幾個東西」。答案是一個官方發行工具鏈(toolchain),其中包含六個角色分工明確的元件。最上層是 rustup,它是版本管理器而非編譯器,負責安裝、移除與切換不同版本與不同目標的 Rust 工具鏈,所有工具鏈檔案都存放在 ~/.rustup 目錄下。
rustc 是真正的編譯器,它把 Rust 原始碼轉譯成機器碼,但在日常開發中你幾乎不直接呼叫它,而是透過 cargo 驅動。cargo 是建置工具與相依套件管理器的合體,它負責解析 Cargo.toml、下載相依套件、決定編譯順序、呼叫 rustc、並提供執行、測試與文件產生等功能。附帶的三個官方工具各司其職:rustfmt 負責程式碼格式化,clippy 是靜態檢查器,提供數百條常見錯誤與不 Idiomatic 寫法的診斷,rustdoc 則從原始碼中的文件註解產生 API 文件。
graph TB
subgraph Users["開發者輸入"]
U1["cargo build / run / test"]
U2["rustfmt"]
U3["cargo clippy"]
U4["cargo doc"]
end
subgraph Rustup["rustup 版本管理器"]
R1["stable"]
R2["beta"]
R3["nightly"]
end
R1 --> C1["rustc 編譯器"]
R1 --> C2["cargo 建置與相依管理"]
R1 --> C3["rustfmt 格式化器"]
R1 --> C4["clippy 靜態檢查器"]
R1 --> C5["rustdoc 文件產生器"]
U1 --> C2
C2 --> C1
C2 --> RM["crates.io 相依套件"]
U2 --> C3
U3 --> C4
U4 --> C5這個分層設計的第一個好處是可替換性。編譯器與建置工具是分開的,因此社群可以寫替代的建置器,或讓 cargo 呼叫外部的 C 編譯器。第二個好處是可攜性:因為 rustup 把所有東西放在使用者目錄下,幾乎不需要系統管理員權限,也幾乎不會與系統套件管理器產生的版本衝突。
三個官方輔助工具值得單獨說明。rustfmt 的設計哲學是「零設定爭議」,它不接受任何風格選項,所有格式規則都寫死在工具裡,唯一的選擇是要不要在 CI 中檢查格式。這種做法徹底消除了團隊內部關於縮排、換行與引號風格的無盡爭論,讓格式差異不再出現在程式碼審查的討論中。clippy 則是把常見的錯誤模式與不建議的寫法編碼成數百條 lint,例如偵測 unwrap() 在可能為空的情況下被使用、建議改用 iter().map(..) 而非手動迴圈、警告不必要的 clone() 呼叫。它與 rustc 共用同一套前端,因此診斷訊息格式一致。
rustdoc 是第三個工具,它從原始碼中的 /// 文件註解產生 API 文件,並且支援在文件中直接執行測試程式碼。cargo doc --open 是最常見的用法,會建置文件並自動開啟瀏覽器。文件中的程式碼範例會在建置時被實際編譯,若範例有誤,建置就會失敗,這個設計讓文件不容易過時。
rustup component 指令用來管理這些元件,rustup component add clippy 安裝、rustup component remove clippy 移除、rustup component list 列出可用項目。元件與工具鏈是綁定的,換了一個工具鏈就要重新安裝元件,這也是為什麼 rust-toolchain.toml 可以聲明 components 欄位的原因。
| 元件 | 角色 | 日常用法 |
|---|---|---|
rustup | 版本與目標管理器 | rustup update、rustup target add |
rustc | 編譯器本體 | 少直接呼叫,多由 cargo 驅動 |
cargo | 建置、相依管理、測試、發布 | cargo build、cargo run、cargo test |
rustfmt | 程式碼格式化器 | cargo fmt,零設定爭議 |
clippy | 靜態檢查器 | cargo clippy,數百條 lint |
rustdoc | 文件產生器 | cargo doc --open |
rust-src | Rust 標準函式庫原始碼 | 供 rust-analyzer 與 nightly 使用 |
除了官方工具之外,社群也提供了編輯器整合的套件,例如 Visual Studio Code 的 rust-analyzer 語言伺服器。rust-analyzer 是以 Rust 實作的,它提供程式碼補全、跳至定義、內嵌提示(hover)與即時診斷。把它設定為編輯器的語言伺服器,可以獲得接近 IDE 的體驗,而這些體驗本質上是「持續執行 cargo check 並把結果送回編輯器」。理解這一點很重要:編輯器中的紅色波浪線並不是編譯器的全部訊息,真正的編譯器仍然是最權威的裁判。
在各平台安裝#
最通用的安裝方式是官方的 rustup 腳本。它會下載並執行一個小型安裝程式,過程中會詢問要安裝的預設工具鏈,建議選擇 stable。
在 Linux 與 macOS 上,最簡單的做法是直接執行:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
這行指令會下載安裝腳本並立即執行。習慣上會先下載再檢查內容,比較符合資安原則:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o rustup-init.sh
sh rustup-init.sh
安裝過程中有三個常見的互動提示。第一個是預設工具鏈的選擇,一般輸入 1 即可選擇 stable;若希望保守起見先看新版變化,可以選 2 進入 beta。第二個是提問是否要安裝 rustfmt 與 clippy,這兩個官方工具幾乎不需要猶豫,應直接接受。第三個是關於 PATH 的說明,安裝程式會修改 ~/.profile 或 ~/.bashrc,在 macOS 上則是 ~/.zshrc,把 ~/.cargo/bin 加入搜尋路徑。
若系統上沒有 curl,多數 Linux 發行版可以使用 wget:
wget --https-only https://sh.rustup.rs -O rustup-init.sh
sh rustup-init.sh
macOS 使用 Homebrew 的使用者也可以安裝 rustup,但要注意 Homebrew 提供的 rust 公式與 rustup 的行為不同,直接安裝 rustup 才是取得多版本管理能力的正確方式:
brew install rustup
rustup-init
Windows 上則是下載 rustup-init.exe 執行安裝程式。安裝過程會詢問預設工具鏈與是否安裝 Visual Studio 的 C++ 建置工具,後者是必要的,因為 Rust 在 Windows 上連結時需要使用微軟的連結器(linker)。
在 Linux 上若要安裝給所有使用者,可以改用套件管理器,例如 Debian 與 Ubuntu 的 apt install rustup,但要注意這類套件版本往往落後官方數個版本,且不提供 rustup 的完整功能。
驗證安裝#
安裝完成後要重新載入 shell 設定(通常是 source ~/.cargo/env),讓 ~/.cargo/bin 進入 PATH。接著用三個指令確認:
rustc --version
cargo --version
rustup --version
正常情況下會看到類似 rustc 1.85.0 (4d91de4e8 2025-02-17) 的輸出,cargo --version 會顯示對應的版本號與編譯日期。三者版本一致代表工具鏈安裝完整。如果 rustc 找不到但 rustup 存在,問題幾乎都在 PATH 設定。
確認安裝是否真正可用,最可靠的方式是編譯並執行一個實際的程式,而不是只看版本號。在 macOS 與 Linux 上可以先建立一個暫存目錄:
mkdir -p ~/rust-smoke-test && cd ~/rust-smoke-test
rustc hello.rs -o hello && ./hello
這個步驟會連同系統的連結器一起驗證,能夠抓出缺少 cc、ld 或微軟連結器等問題。
在 macOS 上,可以把匯出指令寫進 ~/.zshrc:
echo 'source "$HOME/.cargo/env"' >> ~/.zshrc
source ~/.zshrc
多版本與工具鏈管理#
Rust 採每六週一次發行的節奏,三條並行的發行軌道讓不同需求的團隊各取所需。stable 是穩定版軌道,每六週發布一次,正式環境應該使用它。beta 是下一個穩定版的預覽,功能已凍結,主要用來驗證即將到來的變更。nightly 則是每日建置版本,含有尚未穩定的實驗性功能,某些語言特性(例如常數泛型)只有 nightly 才能使用。
選擇哪一條軌道,是一個關於風險的決策。使用 stable 意味著 API 相對穩定,但可能需要等待數月才能取得新功能。使用 nightly 可以第一時間使用最新特性,但必須接受 API 可能隨時改變,因此只應在明確標示的不穩定功能(feature gate)中使用 nightly 專屬特性。實務上常見的折衷是「以 stable 為主,在個別 crate 中用 nightly 特性」,Rust 對此提供了 #![feature(...)] 屬性搭配 cargo +nightly 的工作流。
常見的工具鏈操作如下:
rustup toolchain list
rustup install nightly
rustup default stable
rustup update
rustup component add clippy rustfmt
rustup target add wasm32-unknown-unknown
rustup toolchain list 會標示出目前預設的工具鏈,通常在預設項目旁加上 (default) 標記。若某個專案需要特定版本,可以在其目錄下建立 rust-toolchain.toml 檔案,把版本鎖定:
[toolchain]
channel = "1.85.0"
components = ["rustfmt", "clippy"]
如此一來,進入該目錄後執行任何 cargo 指令都會自動使用指定工具鏈。
目標 triples 與交叉編譯#
Rust 的編譯目標由「目標 triple」標識,例如 x86_64-unknown-linux-gnu 描述的是架構、廠商、作業系統與 ABI。要為其他架構編譯,先安裝該目標的標準函式庫:
rustup target add aarch64-unknown-linux-gnu
cargo build --target aarch64-unknown-linux-gnu
交叉編譯的門檻主要在於連結器與 C 函式庫。Rust 會為目標平台產生物件檔,但最終的連結步驟仍然需要對應平台的 gcc、ld 與 C 執行期。在 Linux 上使用 aarch64-unknown-linux-gnu 時,通常需要安裝 gcc-aarch64-linux-gnu 這類交叉工具鏈套件。對於無法使用 GNU 工具鏈的目標,例如 Apple Silicon 與 Windows ARM,提供 aarch64-apple-darwin 與 aarch64-pc-windows-msvc 等目標。
常見的目標包括 x86_64-unknown-linux-gnu(Linux 桌面)、x86_64-apple-darwin(Intel Mac)、aarch64-apple-darwin(Apple Silicon)、x86_64-pc-windows-msvc(Windows)、x86_64-unknown-linux-musl(使用 musl libc 的靜態連結)以及 wasm32-unknown-unknown(WebAssembly)。
其中 musl 目標值得特別一提。glibc 是 Linux 上最廣泛使用的 C 函式庫,但它的動態載入行為在某些容器環境中會造成相容性問題,musl 目標使用 Alpine Linux 的 musl libc,可以產生體積更小、行為更可預測的執行檔。對於要部署到邊緣運算設備或製作單一檔案程式的情境,x86_64-unknown-linux-musl 是很實用的選擇。
WebAssembly 目標的使用方式稍有不同。Rust 編譯到 wasm32 之後是一個 .wasm 檔案,還需要透過 wasm-bindgen 與 JavaScript 側溝通:
rustup target add wasm32-unknown-unknown
cargo build --target wasm32-unknown-unknown --release
輸出檔案會位於 target/wasm32-unknown-unknown/release/ 目錄下。
離線與 vendoring 的企業考量#
在網路受限或無法連線 crates.io 的環境中,例如部分政府機關或工廠內網,標準做法是將相依套件下載到專案內的 vendor 目錄,稱為 vendoring。這個流程能確保建置的可重現性,同時讓資安稽核得以檢查所有進入建置流程的原始碼。
cargo vendor
這個指令會把所有相依套件的原始碼複製到 vendor 目錄,並印出一段需要加進 .cargo/config.toml 的設定:
[source.crates-io]
replace-with = "vendored-sources"
[source.vendored-sources]
directory = "vendor"
設定完成後,cargo build --offline 就能完全離線建置。專案中的 Cargo.lock 必須一併提交進版控,否則每個人解析出的相依版本可能不同,保證也就失效了。
除了 vendoring,另一種常見做法是設定內部 crate 鏡像伺服器。企業通常會架設一個 crates.io 的代理快取,讓所有建置流量都走內部網路:
[source.crates-io]
replace-with = "internal-mirror"
[source.internal-mirror]
registry = "https://crates.internal.example.com/api/v1/crates"
這兩種做法的共同好處是把「相依套件的來源」變成可稽核、可控管、可重現的東西。當然,鏡像伺服器必須保證其內容與上游 crates.io 一致,否則就等於引入了供應鏈風險。
常見安裝問題#
最普遍的第一個問題是 PATH 未設定。症狀是終端機回報 command not found: cargo,而 ~/.cargo/bin 目錄確實存在。解法是把 source "$HOME/.cargo/env" 加進 shell 設定檔,或手動將該路徑加入 PATH 環境變數。
第二個問題出現在 Windows 上,表現為編譯時出現 linker link.exe not found。這通常代表未安裝 Visual Studio 的 C++ 建置工具,必須透過 Visual Studio Installer 補上「使用 C++ 的桌面開發」工作負載。
第三個問題是代理伺服器。企業環境的 http_proxy 與 https_proxy 環境變數對 rustup 有效,但對 cargo 未必,cargo 使用自己的 HTTP 堆疊,需要在 ~/.cargo/config.toml 中明確設定:
[http]
proxy = "http://proxy.example.com:3128"
[http.certificate]
openssl-ca-certs = "/etc/ssl/certs/ca-certificates.crt"
第四個問題較少見但令人困惑:當系統中存在舊的 rustc(例如 /usr/bin/rustc)時,它可能排在 PATH 的較前面位置,使得 rustup 安裝的版本被遮蔽。執行 which -a rustc 就能看出所有的候選路徑。
還有一類問題來自容器環境。在 Docker 映像中,如果基礎映像已經用套件管理器安裝了 Rust,之後又用 rustup-init.sh 安裝一次,就會出現兩份並存的工具鏈。此時通常的做法是乾淨地移除套件管理器版本,只保留 rustup 的安裝:
apt-get remove -y rustc cargo
rm -rf /usr/local/cargo /usr/local/rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
在持續整合的環境中,這類步驟已經被官方維護的 rust:1.85 之類 Docker 映像封裝好了。直接以這些映像為基底可以省下大量安裝時間,也能確保建置環境與版本完全可重現。
編譯流程的底層#
理解 rustc 內部如何運作,有助於理解編譯時間的成本從何而來。整個流程分成前端與後端兩個階段。前端負責詞法分析、語法分析、型別檢查、借用檢查,並將程式碼轉換成中間表示(MIR,Mid-level Intermediate Representation)。MIR 是一種控制流程圖形式的表示,特別適合做資料流分析,因此所有權與生命週期的檢查幾乎都在這一層完成。
MIR 還讓一類在原始碼層面難以表達的轉換變得容易,例如把 ? 運算子展開成一連串的分支、把 for 迴圈改寫成帶狀態的迴圈結構、把 match 統一成同一種稱為「呼叫」的底層形式。因為 MIR 已經把高階語法結構攤平,後續的最佳化就不必再處理語法層級的複雜性。
graph LR
A[".rs 原始碼"] --> B["rustc 前端<br/>解析、型別檢查、借用檢查"]
B --> C["MIR 中間表示"]
C --> D["MIR 最佳化<br/>內嵌、展開、刪除死碼"]
D --> E["LLVM IR"]
E --> F["LLVM 後端<br/>最佳化與指令選擇"]
F --> G["目標 .o 物件檔"]
G --> H["連結器產生執行檔"]MIR 之後會被轉譯成 LLVM IR,交給 LLVM 後端進行機器碼層級的最佳化與指令選擇,最後由系統的連結器組合成可執行檔。這意味著 Rust 共享了 LLVM 這個成熟的最佳化基礎設施,也意味著「Rust 的效能」與「LLVM 的最佳化能力」在很大程度上是同一件事。
因為 LLVM 是跨平台的,Rust 得以用同一套最佳化基礎設施支援從 x86 到 RISC-V、從 ARM 到 WebAssembly 的大量架構。Rust 官方維護的 tier 分級制度會依據平台的需求量與穩定度,把目標平台分成三個層級:第一層是最常見的平台(例如 x86_64-linux、aarch64-macos),保證隨每個 stable 版本一起發布並接受完整的回歸測試;第二層是需求較少的平台,但仍隨 stable 發布;第三層是僅在 nightly 建置的實驗性平台,只有在沒有其他限制時才會被提升層級。
cargo build 與 cargo build --release 的差異在於 profile。預設的開發 profile 關閉最佳化(opt-level = 0),保留完整的除錯資訊,編譯速度快但執行效率低。Release profile 則啟用最佳化並去除除錯資訊,執行速度顯著提升,但編譯時間更長、除錯體驗變差。兩者的除錯資訊差異可以用最佳化層級來理解:
其中 \(C_{0}\) 是前端與借用檢查的固定成本, \(C_{\text{opt}}\) 是最佳化階段的成本,兩者都可以透過平行編譯分攤。實務上的建議因此是:日常開發用 cargo build 或 cargo check,需要量測效能時才切換到 release。
有一個容易混淆的細節需要澄清:release profile 並非完全「沒有除錯資訊」,而是把除錯資訊的等級從完整降為 line-tables-only,也就是只保留行號對應而移除變數資訊。因此在 release 組態下除錯仍然能看到「這一行是哪一行」,但看不到區域變數的值。想要在 release 環境下做完整的除錯,可以在 profile 中設定 debug = true。
另外,Rust 的最佳化預設不會開啟跨 crate 的連結期最佳化(lto = false),這是為了讓平行編譯能夠運作。完整的 lto = true 能帶來數個百分點的執行期提升,但編譯時間會顯著拉長。
編譯時間的成本與增量編譯#
Rust 的編譯時間長期是社群最常抱怨的議題,尤其對大型專案而言。其主要成本來自單型化:每一個泛型型別參數的組合都會產生一份獨立的機器碼,泛型使用得越廣,產生的程式碼量就越大。同時,借用檢查與生命週期分析屬於全專案層級的分析,無法像單一函式的型別檢查那樣被細緻地快取。
Cargo 提供了幾種緩解手段。增量編譯預設在開發 profile 中啟用(incremental = true),它把中間結果存放在 target/debug/incremental 目錄下,使得第二次建置只重新編譯受影響的 crate。cargo check 只做前端分析,不產生程式碼,因此能省下 MIR 最佳化與 LLVM 後端這兩段最昂貴的時間。codegen-units 設定則控制 LLVM 階段的平行粒度,數值越大並行度越高但最佳化效果稍差。
在大型專案中,模組的拆分方式會直接影響增量編譯的效率:把彼此獨立的功能放進不同的 crate,比放在同一個 crate 的不同模組中更有利,因為 crate 之間的邊界就是編譯並行的邊界。這個道理在實務上常被低估,一個把所有東西塞進單一 lib.rs 的專案,即使功能模組分得很清楚,增量編譯的效益仍然有限。
另一個顯著影響編譯時間的因素是泛型的使用方式。泛型函式在每個被呼叫的地方都會被單型化,如果同一個泛型函式在一百個不同型別上被呼叫,就會產生一百份機器碼。雖然 cargo bloat 這類工具可以協助找出體積膨脹的來源,但真正的解法通常是重新設計抽象的邊界,讓泛型參數停留在較高的層級而不是向下傳播。
值得留意的是,Rust 團隊把「編譯速度」列為語言發展的長期優先事項。Rust 1.75 之後推出的並列前端(parallel frontend)讓多執行緒能同時進行型別檢查與借用檢查,這是近年編譯時間改善的主要來源之一。理解這個背景,才能明白為什麼官方文件反覆建議把 cargo check 當成日常的主要回饋迴圈:它跳過了最昂貴的後端階段,在大型專案上可以省下數倍的等待時間。
工具鏈安裝完成之後,下一節就要真正寫下第一行程式,並觀察編譯器如何與你對話。