Cargo 構建腳本

2021-09-27 14:17 更新

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,frameworkall(默認值)的其中之一,使用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" }

構建腳本可以訪問dependenciesdev-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-searchrustc-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。

這個構建腳本的大部分功能,很容易就重構為常見的依賴項,因此我們的構建腳本不像這個描述那樣長煩! 實際上,通過構建依賴項,構建腳本應該非常簡單。



以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號