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.rstonic_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