안녕하세요, 단테입니다. 오늘은 Cargo와 crate.io의 좀 더 advance한 내용에 대해 이야기해보겠습니다.
러스트에서 release profile
은 코들르 컴파일 할때 커스터마이징한 옵션을 줄 수 있는
설정입니다.
카고는 두 가지의 메인 프로필을 가지고 있습니다.
dev profile
카고는 cargo build
할 때 사용되며 release profile
은 cargo build --release
를 할 때 사용되는 프로필입니다. dev profile은 개발용 옵션들이 기본적으로 적용되어 있으며 release profile은 배포용 옵션들이 적용되어 있습니다.
$ cargo build
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
$ cargo build --release
Finished release [optimized] target(s) in 0.0s
컴파일러는 빌드 옵션에 따라 개발/배포용 프로필을 선택하여 사용합니다.
카고는 [profile.*]를 Cargo.toml 파일에 설정하지 않았을 때는 기본 설정을 사용합니다.
커스터마이징을 위해 [profile.*]
를 작성하면 기본 설정에 대해 오버라이딩 됩니다 예를들어 다음은 dev, release profile을 위한 opt-level 세팅들입니다.ㄴ
// Cargo.toml
[profile.dev]
opt-level = 0
[profile.release]
opt-level = 3
opt-level
세팅은 몇 개의 옵티마이즈를 러스트 컴파일러가 코드에 적용할지를 정하는 것으로 0 부터 3 중에 설정할 수 있습니다. 숫자가 커질수록 더 긴 컴파일 시간이 소요되므로 개발모드에서 컴파일을 자주하는 경우에는 낮은 수의 optimization로 설정할 수 있습니다. 이 경우 런타임의 코드는 더 느려질 수 있습니다.
기본 설정의 opt-level 값은 dev 프로필에서는 0입니다. 배포단계에서는 컴파일에 시간이 오래걸리더라도 실제 코드가 더 빠르게 실행되는게 중요하므로 배포 모드는 컴파일 시간과 실제 코드가 실행되는 속도 간의 트레이드 오프 관계를 가지고 있습니다. 기본 세팅된 release 프로필의 opt-level은 3입니다.
이 세팅 값들을 Cargo.toml
파일에 오버라이딩할 수 있습니다. 예를 들어서 개발용 프로필에 1레벨의 optimization 값을 주고 싶다면, 다음처럼 설정하면 됩니다.
[profile.dev]
opt-level = 1
이 코드는 기본 설정된 0을 오버라이딩 합니다. 이제 cargo build
를 입력하면, 카고는 dev profile에 기본 설정된 여러 값들에 더해서 opt-level에 대해서는 커스터마이징된 값을 사용하게 될 것입니다. opt-level
값을 1로 설정했기 때문에 카고는 좀 더 많은 최적화 작업을 적용할 것 입니다.
node환경에서 npm registry에서 제공하는 패키지를 사용하듯이 러스트에서는 crates.io
에서 제공하는 의존성 패키지들을 사용합니다. 반대로 우리가 작성한 코드들을 사람들과 공유할 수도 있습니다. crates.io
의 crate registry가 우리가 만든 코드와 패키지들을 사람들과 공유하여 이것을 오픈소스화 시킬 수 있습니다.
러스트와 카고는 사람들이 배포한 패키지를 쉽게 사용할 수 있게 도와줍니다.
먼저 만드는 것부터 해봅시다.
정확한 문서화는 사람들이 라이브러리를 사용하기 더욱 용이하게 도와줍니다. 따라서 문서화에 어느정도 시간을 쏟는 것은 그만한 가치가 있습니다. 러스트에서 커멘트를 작성할 때는 두개의 슬래쉬 //를 사용하는데요, 이것 말고도 러스트에서는 documentation comment
라고 하는 코멘트 표기 방법이있습니다. 이것은 html 문서를 만들어줄 것입니다.
문서 코멘트는 세 개의 스래쉬로 사용됩니다. ///
/// Adds one to the number given.
///
/// # Examples
/// ```
/// let answer = my_create::add_one(arg)
///
/// assert_eq!(6, answer);
/// ```
pub fn add_one(x: i32) -> i32 {
x + 1
}
add_one 함수에 대한 설명과 예제 코드를 작성했습니다. Examples라는 Heading 을 작성헀고 이제 cargo doc
을 통해 documentation을 만들 수 있습니다. 이 명령어는 rustdoc
도구를 사용해서 target/doc
디렉토리에 html 문서를 만들어줍니다.
cargo test
는 문서화에 작성된 코드 예제들을 대상으로 테스트를 수행해줍니다. 예제 코드가 잘못 유저들에게 전달되는 것을 방지하기 위해 cargo test를 수행해서 add_one 함수에 대한 테스트를 수행할 수 있습니다.
Doc-tests my_crate
running 1 test
test src/lib.rs - add_one (line 5) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.27s
//! 코멘트가 있는데 보통 create root 파일 내부에 작성하거나 모듈 내부에 작성해서 crate나 모듈에 대해 이야기 합니다.
add_one 함수를 가지고 있는 my_crate
crate를 설명하기 위한 문서화를 추가한다고 하면 //!를 documentation comments 서두에 붙일 수 있습니다.
//! # My Crate
//!
//! `my_crate` is a collection of utilities to make performing certain
//! calculations more convenient.
/// Adds one to the number given.
// --snip--
//!가 마지막으로 작성된 이후 한 줄이 비었습니다. ///말고 //!를 사용했기 때문에 뒤에 따라오는 함수를 설명하는 코멘트가 아니라 이 crate 전체를 설명하는 코멘트가 됩니다.
cargo doc --open
명령어를 수행하면 my_crate라고 불리는 crate에 대한 설명이 public item에 대한 설명 리스트 위에 만들어집니다.
crate를 공개할 때 public apis의 형태를 심사숙고해야 합니다. 사용하는 사람들이 큰 모듈 상하계층에서 어떻게 필요한 부분을 골라사용해야 할지 어려움을 겪기 때문입니다.
예전 강의에서 외부 모듈을 스코프로 사용할 때는 use
키워드를 사용한다고 했었는데요, use mycrate::some_module::another_module::UsefulType
이런식으로 내부 깊숙한 곳에 있는 타입을 가져다 사용하는 것은 유쾌하지 않을 것입니다.
만약 라이브러리 구조가 복잡하게 얽혀있다고 하더라도 다시 이 구조를 헤집어 놓을 필요는 없습니다. re-export 구문을 사용하여 public api들만 한 장소에 모은 다음에 공개할 수 있습니다.
//! # Art
//!
//! A library for modeling artistic concepts.
pub mod kinds {
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
// --snip--
}
}
위 예제를 cargo doc으로 만들게 되면 다음과 같이 됩니다.
PrimaryColor
와SecondaryColor
타입에 대한 정보들은 kinds
, util
를 클릭해야 찾을 수 있습니다.
use art::kinds::PrimaryColor;
use art::utils::mix;
fn main() {
let red = PrimaryColor::Red;
let yellow = PrimaryColor::Yellow;
mix(red, yellow);
}
메인 함수에서 art crate를 사용했습니다. PrimaryColor와 mix가 각각 kinds, mix 모듈에 있다는 사실을 알아야 합니다.
re export를 사용해보겠습니다.
//! # Art
//!
//! A library for modeling artistic concepts.
pub use self::kinds::PrimaryColor;
pub use self::kinds::SecondaryColor;
pub use self::utils::mix;
pub mod kinds {
// --snip--
/// The primary colors according to the RYB color model.
pub enum PrimaryColor {
Red,
Yellow,
Blue,
}
/// The secondary colors according to the RYB color model.
pub enum SecondaryColor {
Orange,
Green,
Purple,
}
}
pub mod utils {
// --snip--
use crate::kinds::*;
/// Combines two primary colors in equal amounts to create
/// a secondary color.
pub fn mix(c1: PrimaryColor, c2: PrimaryColor) -> SecondaryColor {
SecondaryColor::Orange
}
}
cargo doc을 통해 생성되는 문서에는 PrimaryColor, SecondaryColor 타입에 대한 정보가 이전 문서보다 보기 쉽게 나타나있습니다.
이제 mix, PrimaryColor를 사용하기 위해서는 다음과 같이 art 모듈에서 사용하면 됩니다.
use art::{mix, Primarycolor};
fn main() {
// --snip--
}
pub use를 사용해 사용자에게 친화적인 퍼블릭 api 모듈 구조를 제공할 수 있습니다.
터미널에서 cargo login을 통해 crate.io로 이동한 다음, 깃허브 인증과정을 거친 후 token을 생성해 다시 터미널로 돌아와 해당 토큰을 입력한다면 정상적으로 crate.io에 로그인할 수 있을 것입니다.
crate를 만들어 작업한 후에 최종적으로 배포하기 전에 Cargo.toml에 메타데이터를 적을 수 있습니다.
cates.io에 이미 존재하고 있지 않은 crate이름을 선점해보세요. 먼저 중복 패키지 이름이없는지 확인한 이후에 다음 처럼 패키지 이름을 명명합니다.
[package]
name = "guessing_game"
$ cargo publish
Updating crates.io index
warning: manifest has no description, license, license-file, documentation, homepage or repository.
See https://doc.rust-lang.org/cargo/reference/manifest.html#package-metadata for more info.
--snip--
error: failed to publish to registry at https://crates.io
Caused by:
the remote server responded with an error: missing or empty metadata fields: description, license. Please see https://doc.rust-lang.org/cargo/reference/manifest.html for how to upload metadata
description과 license가 누락되었기 때문에 에러가 발생했습니다.
[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"
description = "A fun game where you guess what number the computer has chosen."
license = "MIT OR Apache-2.0"
[dependencies]
$ cargo publish
Updating crates.io index
Packaging guessing_game v0.1.0 (file:///projects/guessing_game)
Verifying guessing_game v0.1.0 (file:///projects/guessing_game)
Compiling guessing_game v0.1.0
(file:///projects/guessing_game/target/package/guessing_game-0.1.0)
Finished dev [unoptimized + debuginfo] target(s) in 0.19s
Uploading guessing_game v0.1.0 (file:///projects/guessing_game)
yarn, pnpm workspace를 사용하여 모노레포를 구성하는 것처럼 cargo workspace를 사용해 점점 커지는 패키지를 파편화할 수 있습니다.
먼저 우리가 workspace를 실습해볼 폴더를 만듭시다. 프로젝트 루트가 되는 디렉토리 입니다.
$ mkdir add
$ cd add
디렉토리를 만들었으면 이제 Cargo.toml 파일을 해당 디렉토리에 듭니다.
// Cargo.toml
[workspace]
members = [
"adder",
]
그 다음 adder package를 만듭니다.
$ cargo new adder
Created binary (application) `adder` package
이 시점에서 우리는 cargo build
를 할 수 있습니다.
이제 전체 폴더 구조는 이렇게 됩니다.
add
├── Cargo.lock
├── Cargo.toml
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
add workspace는 오직 하나의 타멧 디렉토리를 최상위 레벨에 가지고 있습니다. adder 패키지는 타겟 디렉토리가 없습니다. adder 디렉토리 내부에서 cargo build 명령어를 실행한다고 하더라도 별 다른 점이 발생하지 않습니다. 카고는 workspace 내부에 타겟 디렉토리를 만듭니다. 워크스페이스 내부에 있는 create이 다른 crate와 의존관계가 있을 수 있기 때문입니다. 만약 각 crate가 각기 다른 타겟 디렉토리를 갖고 있다면 각 crate는 자기 자신의 타겟디렉토리에 근거해 다시 한번 빌드하는 과정을 거쳐야 할 것입니다. 오직 타겟 디렉토리를 하나만 공유하기 때문에 crate가 빌드될 때 불필요한 빌드를 피할 수 있습니다.
add_one 패키지를 workspace의 새로운 패키지로 만들겠습니다. 최상단 Cargo.toml에 add_one 경로를 members 리스트에 포함시킵시다.
[workspace]
members = [
"adder",
"add_one",
]
다음 명령어를 통해 새로운 라이브러리 crate를 생성합니다.
$ cargo new add_one --lib
Created library `add_one` package
├── Cargo.lock
├── Cargo.toml
├── add_one
│ ├── Cargo.toml
│ └── src
│ └── lib.rs
├── adder
│ ├── Cargo.toml
│ └── src
│ └── main.rs
└── target
add_one/src/lib.rs
파일에 add_one 함수를 생성합니다.
pub fn add_one(x: i32) -> i32 {
x + 1
}
adder패키지에서 add_one 패키지를 디펜던시로 갖게 합니다.
Cargo는 workspace 내부에 있는 crate들끼리 의존 관계를 갖는다고 생각하지 않으므로 명시적으로 [dependencies]
를 설정해주어야 합니다.
// adder/Cargo.toml
[dependencies]
add_one = { path = "../add_one" }
// adder/src/main.rs
use add_one;
fn main() {
let num = 10;
println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}
이제 빌드합니다.
$ cargo build
Compiling add_one v0.1.0 (file:///projects/add/add_one)
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 0.68s
add 디렉토리 내부에 있는 binary crate를 실행시키고 싶으면 패키지 이름을 명시해서 -p 옵션과 함께 실행시킵니다.
$ cargo run -p adder
Finished dev [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/adder`
Hello, world! 10 plus one is 11!
Cargo.lock파일은 워크 스페이스 내부 가장 상위 레벨에만 존재하는 것을 볼 수 있습니다. 이것은 모든 crate가 같은 버전의 의존성을 사용하는 것을 보장합니다. adder/Cargo.toml. add_one/Cargo.toml 파일에 rand 의존성을 추가하면 Cargo.lock에 명시된 같은 버전을 사용합니다.
add_one crate에서 rand를 사용해야 한다고 가정합시다.
// add_one/Cargo.toml
[dependencies]
rand = "0.8.5"
add_one/src/lib.rs
파일에 use rand;
statement를 선언했습니다. 하지만 이후 빌드 과정에서 경고 문구가 나오게 됩니다.
$ cargo build
Updating crates.io index
Downloaded rand v0.8.5
--snip--
Compiling rand v0.8.5
Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
--> add_one/src/lib.rs:1:5
|
1 | use rand;
| ^^^^
|
= note: `#[warn(unused_imports)]` on by default
warning: `add_one` (lib) generated 1 warning
Compiling adder v0.1.0 (file:///projects/add/adder)
Finished dev [unoptimized + debuginfo] target(s) in 10.18s
최상단 Cargo.lock 파일에서 rand에 대한 add_one의 종속성 정보를 가지고 있습니다.
add 패키지에서 add_one을 사용하므로 이 패키지를 사용하는 다른 crate에도 해당 rand의 종속성 정보를 명시해야 합니다. 두 crate에 동일 종속성을 명시하더라도 카고는 종속성을 중복해서 다운받지 않습니다.
오늘은 cargo / crate.io에 대한 고급 개념을 알아보았습니다.
공부하시느라 수고 많으셨습니다.
감사합니다.!