Rust 프로젝트에서 third-party non-Rust 코드를 함께 컴파일해야 할 때 빌드 스크립트를 이용할 수 있다. 별도의 스크립트를 실행하지 않고 프로젝트에 통합할수 있다는 점이 굉장히 유용한 편!
이 스크립트를 사용하는 예제로는
bindgen
, cc
등을 이용하여 non-Rust 코드를 컴파일, Rust 코드와 연결해야 할 때
gRPC
, jsonschema
등을 사용해 정의된 타입을 변환할 때
등이 있다. 이 스크립트를 사용할 때 주의해야 하는 소소한 점들을 정리했다.
build.rs 예제#
Cargo는 컴파일 과정에서 build script를 빌드, 실행한다. 따라서 다음과 같은 빌드 스크립트는 패키지 컴파일 과정에서 hello.rs
파일을 만들게 된다. 패키지의 코드는 hello.rs
내 함수를 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// build.rs
use std::env;
use std::fs;
use std::path::Path;
fn main() {
let out_dir = env::var_os("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("hello.rs");
fs::write(
&dest_path,
"pub fn message() -> &'static str {
\"Hello, World!\"
}
"
).unwrap();
}
|
Cargo.toml
의 package 옵션에 스크립트를 특정 파일로 지정할 수도 있다.
1
2
3
4
5
|
[package]
name = "build_example"
version = "0.1.0"
edition = "2021"
build = "build.rs" # <- 바꿀 수 있음
|
build.rs dependency#
빌드 스크립트를 사용하는 대표적인 경우 중 하나가 proto 파일로 정의된 gRPC를 사용할 때이다. tonic을 사용하면, proto에 정의된 대로 서버, 클라이언트 코드를 생성할 수 있다. 이 과정은 빌드 스크립트에서 일어난다.
1
2
3
4
5
6
7
8
9
10
11
|
// build.rs
fn main() {
tonic_build::configure()
.build_server(false)
.out_dir("src/rpc")
.compile(
&["proto/googleapis/google/pubsub/v1/pubsub.proto"],
&["proto/googleapis"],
)
.unwrap();
}
|
이 때, build.rs
는 tonic_build
를 사용한다. 여기서 쓰는 dependency는 Cargo.toml 파일 build-dependencies
항목에 추가해 줘야 한다. 소소하지만 깜빡하기 쉬운 포인트!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
# Cargo.toml
[package]
name = "build_example"
version = "0.1.0"
edition = "2021"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tonic = "0.10.2"
tokio = { version = "1.35.1", features = ["rt", "rt-multi-thread", "macros"] }
prost = "0.12.3"
[build-dependencies]
tonic-build = "0.10.2" # <---- 여기 추가해 줘야 함 !
|
build.rs 재 실행#
build.rs
는 해당 파일의 변경점이 있을 때 재컴파일 되고, “패키지 내의 어떤 파일이 변경되면” 재 실행된다.
위의 gRPC 예제에서 proto 파일이 패키지 내에 있다면, proto 파일이 변경될 때 마다 코드는 새로 생성된다.
하지만 다음과 같은 구조로, 패키지 외부에 정의된 proto 파일(api.proto
)을 참조하는 경우에는 해당 파일이 변경되어도 빌드 스크립트가 재실행되지 않는다.
1
2
3
4
5
6
7
|
.
├── frontend
├── api.proto // <-- 참고하는 proto 파일
└── rust_backend // <-- package
├── Cargo.toml
├── build.rs
└── main.rs
|
이 경우에는 다음과 같이 빌드 스크립트가 재실행되는 조건을 명시할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// build.rs
fn main() {
println!("cargo:rerun-if-changed=../api.proto"); // <-- 이 명령을 출력!
tonic_build::configure()
.build_server(false)
.out_dir("src/rpc")
.compile(
&["proto/googleapis/google/pubsub/v1/pubsub.proto"],
&["proto/googleapis"],
)
.unwrap();
}
|
패키지 외부의 파일을 참조할 때, 패키지가 매우 커 모든 변경 시 마다 실행하기 부담스러울 때 등에 활용하면 될 듯 하다.
build.rs에서 src 참조하기#
build.rs 내에서 src의 함수/자료구조 등에 접근하는 건 불가능하다. 반대도 마찬가지. stackoverflow나 reddit을 찾아보면 이 경우 crate를 분리하라고 한다. 즉, 참조해야 하는 내용을 별도의 crate로 분리하고 이를 build dependency로 추가하라는 것.
1
2
3
4
5
6
7
8
9
10
|
.
├── the_library
│ ├── Cargo.toml
│ ├── build.rs
│ └── src
│ └── lib.rs
└── the_type
├── Cargo.toml
└── src
└── lib.rs // <-- 참조하고 싶은 type만 별도의 crate로 분리
|
1
2
3
4
|
# the_library/Cargo.toml
# add dependency to the library
[build-dependencies]
the_type = { path = "../the_type" }
|
src 파일과 빌드 스크립트가 서로를 참조하는 경우는 없어야 하는 것 같다.
References#