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
)을 참조하는 경우에는 해당 파일이 변경되어도 빌드 스크립트가 재실행되지 않는다.
├ ├ └ ─ ─ ─ ─ ─ ─ f a r ├ ├ └ r p u ─ ─ ─ o i s ─ ─ ─ n . t t p _ C b m e r b a u a n o a r i i d t c g l n o k o d . e . . r n t r s d o s m l 참 고 하 는 p a p c r k o a t g o e 파 일
이 경우에는 다음과 같이 빌드 스크립트가 재실행되는 조건을 명시할 수 있다.
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로 추가하라는 것.
├ │ │ │ │ └ ─ ─ ─ ─ t ├ ├ └ t ├ └ h ─ ─ ─ h ─ ─ e ─ ─ ─ e ─ ─ _ _ l C b s └ t C s └ i a u r ─ y a r ─ b r i c ─ p r c ─ r g l e g a o d l o l r . . i . i y t r b t b o s . o . m r m r l s l s 참 조 하 고 싶 은 t y p e 만 별 도 의 c r a t e 로 분 리 1
2
3
4
# the_library/Cargo.toml
# add dependency to the library
[build-dependencies]
the_type = { path = "../the_type" }
src 파일과 빌드 스크립트가 서로를 참조하는 경우는 없어야 하는 것 같다.
References#