Build Scripts
構建腳本
一些包需要編譯第三方非 Rust 代碼,例如 C 庫。其他的包需要鏈接到 C 庫,當然這些庫既可以位于系統(tǒng)上,也可以從源代碼構建。其他人或許還需要功能工具,比如構建之前的代碼生成(想想解析生成器)。
Cargo 并不打算替換為這些能良好優(yōu)化任務的其他工具,但是它與build
配置選項.
[package]
# ...
build = "build.rs"
指定的build
命令應執(zhí)行的 Rust 文件(相對于包根),將在包編譯其他內容之前,被編譯和調用,從而具備 Rust 代碼所依賴的構建或生成的工件。默認情況下 Cargo 在包根文件中尋找"build.rs"
(即使您沒有給build
字段指定值)使用build = "custom_build_name.rs"
指定自定義生成名,或build = false
禁用對構建腳本的自動檢測。
Build 命令的一些用例是:
- 構建一個捆綁的 C 庫.
- 在主機系統(tǒng)上找到 C 庫.
- 從規(guī)范中生成 Rust 模塊.
- 為箱,執(zhí)行所需的某平臺特定配置.
下面將詳細介紹每一個用例,以給出構建命令如何工作的示例.
Inputs to the Build Script
輸入到構建腳本
當運行構建腳本時,存在許多構建腳本用到的輸入,所有輸入都以環(huán)境變量傳入。
除了環(huán)境變量之外,構建腳本的當前目錄是構建腳本包的源目錄.
Outputs of the Build Script
構建腳本的輸出
由構建腳本打印到 stdout 的所有行都被寫入像target/debug/build/<pkg>/output
這樣的文件(精確的位置可能取決于你的配置)。如果您希望直接在終端中看到這樣的輸出,那么使用非常詳細-vv
標志。注意,如果既不修改構建腳本也不修改包源文件,下一次的-vv
調用將不打印重復輸出到終端,因為沒有執(zhí)行新的構建??蓤?zhí)行cargo clean
,如果希望確保輸出始終顯示在終端上,但要在每次 Cargo 調用之前執(zhí)行。任何一行以cargo:
開始的,直接由 Cargo 解釋。行必須是cargo:key=value
形式,就像下面的例子:
# specially recognized by Cargo
cargo:rustc-link-lib=static=foo
cargo:rustc-link-search=native=/path/to/foo
cargo:rustc-cfg=foo
cargo:rustc-env=FOO=bar
# arbitrary user-defined metadata
cargo:root=/path/to/foo
cargo:libdir=/path/to/foo/lib
cargo:include=/path/to/foo/include
另一方面,打印到 stderr 的行被寫入像target/debug/build/<pkg>/stderr
這樣的文件,但不被 Cargo 解釋。
Cargo 識別一些特殊的 key,其中一些影響箱的構造:
rustc-link-lib=[KIND=]NAME
說明了,指定值是庫名,且會作為-l
標志傳遞給編譯器。KIND
可選為static
,dylib
(默認值),或framework
的其中之一,用rustc --help
見更多細節(jié)。rustc-link-search=[KIND=]PATH
說明了,指定值是庫搜索路徑,且會作為-L
標志傳遞給編譯器。KIND
可選為dependency
,crate
,native
,framework
或all
(默認值)的其中之一,使用rustc --help
見更多細節(jié).rustc-flags=FLAGS
是傳遞給編譯器的一組標志,僅支持-l
和-L
標志。rustc-cfg=FEATURE
說明了,指定的特性,且會作為--cfg
標志傳遞給編譯器。這通常對檢測,執(zhí)行各種特征的編譯時間,是有用的。rustc-env=VAR=VALUE
說明了,指定的環(huán)境變量,且會被添加到編譯器所在的環(huán)境中。然后,可以通過編譯箱中的env!
宏檢索該值。這對于在箱的代碼中嵌入額外的元數據很有用,比如 Git HEAD 的散列,或持續(xù)集成服務器的唯一標識符。rerun-if-changed=PATH
是文件或目錄的路徑,說明了如果構建腳本發(fā)生更改(由文件上最近修改的時間戳檢測到),則應重新運行構建腳本。通常,如果箱根目錄中的任何文件發(fā)生更改,則重新運行構建腳本,但這可用于將更改范圍擴展到僅一小組文件。(如果這個路徑指向一個目錄,則不會遍歷整個目錄以進行更改——只對目錄本身的時間戳進行更改(該時間戳對應于目錄中的某些類型的更改,取決于平臺),將觸發(fā)重新構建。要請求重新運行整個目錄中的任何更改,請遞歸地為該目錄打印一行,為該目錄內的所有內容打印另一行。)請注意,如果構建腳本本身(或其依賴項之一)更改,則無條件地重新構建和重新運行該腳本,因此,cargo:rerun-if-changed=build.rs
幾乎總是冗余(除非您想要忽略除了build.rs
,所有其他文件的變化)rerun-if-env-changed=VAR
是環(huán)境變量的名稱,說明了它指示如果環(huán)境變量的值發(fā)生變化,則應重新運行構建腳本。這基本上與rerun-if-changed
是一樣的,除了它與環(huán)境變量一起工作。注意,這里的環(huán)境變量用于全局環(huán)境變量,如CC
這樣的,對于 Cargo 所設的像TARGET
,就不必使用它。還要注意,如果rerun-if-env-changed
打印出來,然后 Cargo 將只在,那些環(huán)境變量發(fā)生變化,或者打印出rerun-if-changed
改變的文件的情況下,才重新運行構建腳本。warning=MESSAGE
是構建腳本運行完畢后,打印到主控制臺的消息/警告只針對路徑依賴項(即,您在本地工作的那些依賴項)顯示,因此如, crates.io 的箱在默認情況下不會打印警告。
其他哪些元素都是用戶定義的元數據,這些元數據傳遞給了依賴的。關于這個的更多信息可以在links
部分查看.
Build Dependencies
構建依賴
構建腳本也可以依賴其他基于 Cargo 的箱。依賴關系通過清單的build-dependencies
部分指定。
[build-dependencies]
foo = { git = "https://github.com/your-packages/foo" }
構建腳本不可以訪問dependencies
或dev-dependencies
部分列表中的依賴項(它們還沒有建成!),除非明確聲明,否則包本身也不能使用所有構建依賴項。
The links
Manifest Key
links
清單 鍵
除了清單鍵build
,Cargo 也支持一個,要鏈接到本地庫的名稱聲明,那就是links
清單鍵:
[package]
# ...
links = "foo"
build = "build.rs"
此清單說明了包會鏈接到本機庫libfoo
,并且它還具有定位和/或構建該本機庫的構建腳本。Cargo 要求build
如果有值,那links
也要有值。
這個清單鍵的目的是,讓 Cargo 了解包所具有的本地依賴項集合,并提供在包構建腳本之間,傳遞元數據的合適的系統(tǒng).
首先,Cargo 要求一個包最多只有一個links
值。換句話說,禁止兩個包鏈接到同一個本機庫。然而,這里也有約定位置的方式,用來緩解這個問題。
如上面在輸出格式中提到的,每個構建腳本可以以鍵-值對的形式生成一組任意的元數據。此元數據傳遞給依賴的包。例如,如果libbar
依賴libfoo
,當libfoo
生成key=value
作為其元數據的一部分,那libbar
的構建腳本會有DEP_FOO_KEY=value
環(huán)境變量。
注意,元數據只傳遞給直接依賴項,而不是把依賴項串起來。此元數據傳遞的動機,會在接下來,關聯到系統(tǒng)庫案例研究中概述。
Overriding Build Scripts
覆蓋 構建腳本
如果一個清單包含links
關鍵字,那 Cargo 支持重寫用自定義庫指定的構建腳本。此功能的目的是防止完全運行有問題的構建腳本,而是提前提供下元數據。
要覆蓋構建腳本,請將下列配置放在任何可接受的 Cargo 的配置位置中。
[target.x86_64-unknown-linux-gnu.foo]
rustc-link-search = ["/path/to/foo"]
rustc-link-lib = ["foo"]
root = "/path/to/foo"
key = "value"
本節(jié)說明目標x86_64-unknown-linux-gnu
,命名為foo
的庫,具有指定的元數據。此元數據與構建腳本時生成的元數據相同,提供了許多鍵/值對,其中rustc-flags
,rustc-link-search
和rustc-link-lib
有點特殊.
使用此配置,如果一個包聲明它鏈接到此foo
,那構建腳本將不編譯或運行,而會使用指定的元數據。
Case study: Code generation
案例學習: 代碼生成
由于各種原因,一些 Cargo 包在編譯之前需要生成代碼。這里我們將介紹一個簡單的示例,該示例把,'生成庫調用'作為構建腳本的一部分.
首先,讓我們看一下這個包的目錄結構:
.
├── Cargo.toml
├── build.rs
└── src
└── main.rs
1 directory, 3 files
在這里我們可以看到我們有一個build.rs
構建腳本,和二進制文件main.rs
。 接下來,讓我們看一下清單:
# Cargo.toml
[package]
name = "hello-from-generated-code"
version = "0.1.0"
authors = ["you@example.com"]
build = "build.rs"
在這里,我們可以看到,我們已經指定了一個構建腳本build.rs
,我們將使用它來生成一些代碼。讓我們看看構建腳本里面有什么:
// build.rs
use std::env;
use std::fs::File;
use std::io::Write;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("hello.rs");
let mut f = File::create(&dest_path).unwrap();
f.write_all(b"
pub fn message() -> &'static str {
\"Hello, World!\"
}
").unwrap();
}
這里有兩點值得注意的地方:
- 腳本使用
OUT_DIR
環(huán)境變量,以知道輸出文件到哪里。它可以使用進程的當前工作目錄,來查找輸入文件應該到哪里,但是在這種情況下,我們是沒有任何輸入文件的。 - 一般來說,構建腳本不應該修改
OUT_DIR
目錄外的任何文件。 乍看之下,似乎不錯,但當您使用這種箱子作為依賴項時,它確會帶來問題,因為.cargo/registry
源中的隱性的常量應該是不變的。cargo
在打包時不會允許這樣的腳本。 - 這個腳本相對簡單,只是寫出一個小生成的文件??梢韵胂螅渌嫣氐牟僮饕部赡馨l(fā)生,例如從 C 頭文件或其他定義的語言生成 Rust 模塊。
接下來,我們來看看庫本身:
// src/main.rs
include!(concat!(env!("OUT_DIR"), "/hello.rs"));
fn main() {
println!("{}", message());
}
這就是真正的魔法發(fā)生的地方。該庫正在使用 rustc 定義的 include!
宏,它又結合concat!
與env!
宏去包含生成文件(hello.rs
),從而進入箱的編譯。
使用此處所示的結構,箱可以包括(include)構建腳本在內的,任何數量的生成文件。
Case study: Building some native code
案例學習: 構建一些原生代碼
有時需要建立一些本地 C 或 C++代碼作為包的一部分。這是在用構建腳本到 Rust 箱本身之前,構建本機庫的另一個極好用例。作為一個例子,我們將創(chuàng)建一個 Rust 庫,它調用 C 來打印"Hello,World!".
和上面一樣,讓我們先來看看包的布局:
.
├── Cargo.toml
├── build.rs
└── src
├── hello.c
└── main.rs
1 directory, 4 files
很像之前的吧! 下一步,清單如下:
# Cargo.toml
[package]
name = "hello-world-from-c"
version = "0.1.0"
authors = ["you@example.com"]
build = "build.rs"
現在,我們不打算使用任何-構建的依賴項,所以現在讓我們看一下構建腳本:
// build.rs
use std::process::Command;
use std::env;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
// 請注意,這種方法存在許多缺點,
// 下個代碼展示,會詳細介紹如何提高這些命令的可移植性。
Command::new("gcc").args(&["src/hello.c", "-c", "-fPIC", "-o"])
.arg(&format!("{}/hello.o", out_dir))
.status().unwrap();
Command::new("ar").args(&["crus", "libhello.a", "hello.o"])
.current_dir(&Path::new(&out_dir))
.status().unwrap();
println!("cargo:rustc-link-search=native={}", out_dir);
println!("cargo:rustc-link-lib=static=hello");
}
此構建腳本首先將 C 文件編譯為對象文件(通過調用gcc
),然后將這個對象文件轉換為靜態(tài)庫(通過調用ar
),最后一步是反饋給 Cargo ,以表示我們的輸出在out_dir
和通過-l static=hello
標志,編譯器應該將箱靜態(tài)鏈接到libhello.a
。
請注意,這種硬編碼方法有許多缺點:
- 這個
gcc
命令本身不是跨平臺可移植的。如,在 Windows 平臺不太可能gcc
,甚至不是所有 UNIX 平臺都可能有gcc
。 這個ar
命令也處于類似的情況。 - 這些命令不考慮跨編譯。如果我們?yōu)?Android 這樣的平臺進行跨編譯,
gcc
就不太可能產生一個可執(zhí)行的 ARM.
但不要害怕,這里build-dependencies
就幫到你! Cargo 生態(tài)系統(tǒng)有許多包,為了使此類任務更加容易、可移植和標準化。構建腳本可以寫成:
// build.rs
// 依賴于外部維護的`cc`包,管理
// 調用C編譯器。
extern crate cc;
fn main() {
cc::Build::new()
.file("src/hello.c")
.compile("hello");
}
添加cc
箱,這樣將構建,依賴cc
就好啦,將下面的添加到您的Cargo.toml
:
[build-dependencies]
cc = "1.0"
這個cc
箱抽象了 C 代碼構建,主要用于腳本需求范圍:
- 它調用適當的編譯器(Windows 的 MSVC,
gcc
對 MinGW ,cc
對 UNIX 平臺等等). - 通過向正在使用的編譯器傳遞適當的標志,獲取
TARGET
變量. - 其他環(huán)境變量,如
OPT_LEVEL
,DEBUG
等等,都是自動處理的. - stdout 輸出和
OUT_DIR
位置也由cc
庫控制.
在這里,我們可以開始看到,將盡可能多的功能移植到公共構建依賴項,而不是在所有構建腳本之間復制來復制去,的一些主要好處!
回到案例研究,讓我們快速瀏覽一下src
目錄中的內容:
// src/hello.c
#include <stdio.h>
void hello() {
printf("Hello, World!\n");
}
// src/main.rs
// 注意缺少`#[link]`屬性。 我們選擇,將責任委派給
// 構建腳本的鏈接,而不是硬編碼
// 它在源文件中.
extern { fn hello(); }
fn main() {
unsafe { hello(); }
}
然后,就好啦! 這就完成了使用構建腳本,從 Cargo 包構建一些 C 代碼的示例。這也說明了為什么在許多情況下使,用構建依賴項非常重要,甚至更加簡潔!
我們還看到了構建腳本使用箱,純粹作為用于構建過程的依賴項,而不是在運行時,用作箱本身的依賴項的簡要示例。
Case study: Linking to system libraries
案例學習: 鏈接到系統(tǒng)庫
這里的最后一個案例研究,將研究 Cargo 庫如何鏈接到系統(tǒng)庫,以及構建腳本如何支持這個用例。
通常,Rust 箱希望鏈接到系統(tǒng)上經常提供的本地庫,以綁定其功能,或者只是將其用作實現細節(jié)的一部分。想以不管平臺的方式執(zhí)行這個操作,而這卻是一個相當微妙的問題,再次說明下,構建腳本的目的是盡可能多地分配這些(微妙)內容,以便讓消費者盡可能容易地使用它.
作為一個例子,讓我們來看一個Cargo 本身的依賴,libgit2。這個 C 庫其實有許多約束條件:
- 它可選為依賴 Unix 上的 OpenSSL ,來實現 https 傳輸.
- 它可選為依賴所有平臺上的 libssh2 ,來實現 ssh 傳輸.
- 默認情況下,它通常不安裝在所有系統(tǒng)上.
- 它可以從源代碼使用
cmake
構建.
為了可視化這里發(fā)生的事情,讓我們看一下,鏈接本機 C 庫的相關 Cargo 包的清單。
[package]
name = "libgit2-sys"
version = "0.1.0"
authors = ["..."]
links = "git2"
build = "build.rs"
[dependencies]
libssh2-sys = { git = "https://github.com/alexcrichton/ssh2-rs" }
[target.'cfg(unix)'.dependencies]
openssl-sys = { git = "https://github.com/alexcrichton/openssl-sys" }
# ...
正如上面的清單所顯示的,我們指定了一個build
腳本,但值得注意的是,該示例具有links
項,說明該箱(libgit2-sys
)鏈接到了這個本地庫git2
。
在這里,我們還看到,我們選擇讓 Rust 箱有一個無條件的,通過libssh2-sys
箱依賴libssh2
(ssh2-rs),以及(有條件的)特定于平 unix 臺的openssl-sys
依賴(其他平臺現在被漠視)。這似乎有點違反在 Cargo 清單 的 C 依賴 的明確性,但這實際上是這'地方'中使用 Cargo 的一種約定.
*-sys
Packages
*-sys
包們
為了減輕對系統(tǒng)庫的鏈接,crates.io 有一個包命名和功能的慣例。比如包名foo-sys
,它應該提供兩個主要功能:
- 庫箱應鏈接到本地庫
libfoo
。 在源代碼最后構建之前,這將經常探測當前的系統(tǒng)的libfoo
。 - 庫箱應提供在
libfoo
的聲明函數,但是不綁定或高級抽象。
一套*-sys
包,提供了一組用于連接到本地庫的公共依賴項。通過這種'本機庫相關'的包約定,可以獲得許多好處:
foo-sys
的公共依賴,會減輕上面所說的,關于一個包的links
的每個值規(guī)則。- 一個公共依賴關系,更能發(fā)現
libfoo
本身的集中邏輯(或者從源代碼構建它). - 這些依賴關系很容易被重寫.
Building libgit2
構建 libgit2 吧
現在我們已經整理了 libgit2 的依賴,我們需要實際編寫下構建腳本。我們這里不討論特定的代碼片段,而只研究libgit2-sys
構建腳本的高層細節(jié)。這并不是建議所有包都遵循這個策略,而僅概述一個特定的策略。
構建腳本應該做的第一步是查詢 libgit2 是否已經安裝在主機系統(tǒng)上。要做到這一點,我們將利用現有的工具pkg-config
(當它可用時)。我們也會使用build-dependencies
部分重構成pkg-config
相關的所有代碼(或者有人已經這樣做了!)。
如果pkg-config
找不到 libgit2,或者如果pkg-config
只是沒有安裝,下一步就要從捆綁源代碼構建 libgit2 (捆綁源碼作為libgit2-sys
本身的一部分)。然而,在這樣做時有一些細微差別,我們需要加以考慮:
libgit2 的構建系統(tǒng),
cmake
需要能夠找到 libgit2 可選依賴 libssh2 。而我們確信我們已經構建了它(因它是一個 Cargo 依賴項),我們只需要傳遞這個信息。為此,我們利用元數據格式,在構建腳本之間傳遞信息。在這個例子中,打印出的 libssh2 包信息是cargo:root=...
,它來告訴我們 libssh2 安裝在哪里,然后我們可以通過CMAKE_PREFIX_PATH
環(huán)境變量讓 cmkae 知道。我們需要處理下,編譯 C 代碼時的一些
CFLAGS
值(也要告訴cmake
關于這個信息)。我們想傳遞的一些標志是 64 位的-m64
,32 位的-m32
,或-fPIC
也適用于 64 位。最后,我們調用
cmake
將所有輸出放入環(huán)境變量OUT_DIR
目錄,然后打印必要的元數據,以指導 rustc 如何鏈接到 libgit2。
這個構建腳本的大部分功能,很容易就重構為常見的依賴項,因此我們的構建腳本不像這個描述那樣長煩! 實際上,通過構建依賴項,構建腳本應該非常簡單。
更多建議: