Giá trị có tính phân biệt cao (high-cardinality) cho các flags build trong Rust
Khi làm một bản demo về WebAssembly và Kubernetes, tôi muốn tạo ba binary khác nhau từ cùng một đoạn mã:
- Native: biên dịch mã Rust thành mã máy gốc để làm chuẩn so sánh.
- Embed: biên dịch thành WebAssembly và dùng image Docker của WasmEdge làm image nền.
- Runtime: biên dịch thành WebAssembly, sử dụng một image nền rỗng (scratch), và thiết lập runtime khi chạy mã.
Đoạn mã là một máy chủ HTTP với một endpoint duy nhất. Để phục vụ mục đích demo, tôi muốn endpoint này trả về kiểu của image đang chạy.
curl localhost:3000/get
{"source": "native", "data": {}}
Ý tưởng là có một codebase duy nhất có thể biên dịch thành mã gốc hoặc WebAssembly. Tôi giải quyết yêu cầu này bằng cách sử dụng flag biên dịch cfg
:
#[cfg(flavor = "native")]
const FLAVOR: &str = "native";
#[cfg(flavor = "embed")]
const FLAVOR: &str = "embed";
#[cfg(flavor = "runtime")]
const FLAVOR: &str = "runtime";
// Use FLAVOR later in the returned HTTP response
Build Scripts trong Rust
Một số package cần biên dịch mã không phải Rust (ví dụ thư viện C). Một số khác cần liên kết với thư viện C có sẵn hoặc phải được build từ mã nguồn. Một số cần thực hiện sinh mã trước khi build (ví dụ bộ sinh parser).
Cargo không thay thế các công cụ chuyên dụng cho các tác vụ này, nhưng có thể tích hợp với chúng thông qua build script. Đặt một file tên build.rs
ở thư mục gốc của package sẽ khiến Cargo biên dịch và thực thi script đó ngay trước khi build package.
Thử nghiệm với build script đơn giản
Tạo một file build.rs
ở thư mục gốc của crate:
fn main() {
println!("Hello from build script!");
}
cargo build
Compiling high-cardinality-cfg-compile v0.1.0 (/Users/nico/projects/private/high-cardinality-cfg-compile)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.08s
Kết quả build xong nhưng không có log nào hiển thị — vì theo tài liệu:
Output của build script sẽ bị ẩn khi biên dịch bình thường. Nếu bạn muốn thấy output, hãy dùng flag
-vv
.
Chạy lại:
cargo build --vv
Bây giờ log sẽ hiển thị như mong đợi:
...
[high-cardinality-cfg-compile 0.1.0] Hello from build script!
...
Tạo file mã từ script thay vì hard-code
Tôi sẽ dùng build script để tạo một file Rust chứa giá trị flavor được truyền vào từ metadata trong Cargo.toml
.
Vì build script chạy trước khi biên dịch nên nó không có quyền truy cập vào các flag cfg
. Thay vào đó, tôi sẽ truyền giá trị này thông qua package.metadata
trong Cargo.toml
:
[package.metadata]
flavor = "foobar"
Sau đó thêm dependency vào build-dependencies
:
[build-dependencies]
cargo_metadata = "0.19.1"
Build script:
fn main() {
let metadata = MetadataCommand::new() //1
.exec()
.expect("Failed to fetch cargo metadata");
let package = metadata.root_package().expect("No root package found"); //2
let flavor = package //3
.metadata
.get("flavor")
.and_then(|f| f.as_str())
.expect("flavor is not set in Cargo.toml under [package.metadata]");
let dest_path = Path::new("src").join("flavor.rs"); //4
fs::write(&dest_path, format!("pub const FLAVOR: &str = \"{}\";\n", flavor)) //5
.expect("Failed to write flavor.rs");
println!("cargo:rerun-if-changed=Cargo.toml");
println!("cargo:warning=FLAVOR written to {}", dest_path.display());
}
Giải thích:
- Đọc metadata từ
Cargo.toml
- Xác định package chính
- Lấy giá trị
flavor
- Tạo file
src/flavor.rs
- Ghi nội dung hằng số vào file
Sử dụng trong mã chính
Trong mã nguồn:
mod flavor;
fn main() {
println!("Hello from flavor {}", flavor::FLAVOR);
}
Khi chạy chương trình:
cargo run
Hello from flavor foobar
Kết luận
Trong bài viết này, tôi đã trình bày cách sử dụng các tham số build có tính phân biệt cao bằng metadata trong Cargo.toml
. Tôi cũng có thể dùng biến môi trường, nhưng mục đích là để học cách sử dụng metadata.
File build.rs
là một thủ thuật rất hữu ích để đạt được các mục tiêu không thể thực hiện được bằng tính năng tiêu chuẩn của Cargo.
All rights reserved