Danh sách bài viết

Bài 21: cargo build vs cargo run vs cargo check

Bài 21 của series Rust Cơ Bản — phân biệt rõ 3 lệnh hay nhầm: cargo check chỉ chạy typecheck + borrow check, không link, nhanh gấp 2-3 lần build; cargo build compile + link ra executable trong target/debug/; cargo run gộp build + execute trong một bước. Kèm cargo build --release cho binary tối ưu, flag --bin chọn binary, --example chạy demo, --features bật tính năng optional, và workflow khuyến nghị với cargo-watch để auto rebuild khi save.

09/06/2026
10 phút đọc
0 lượt xem
1

Mục Tiêu Bài Học

Sau bài học, bạn sẽ:

  • Hiểu rõ cargo check làm gì và vì sao nó nhanh hơn cargo build 2-3 lần.
  • Biết khi nào dùng cargo build (compile + link) vs cargo run (build + execute liền).
  • Phân biệt profile dev (default) và release (--release), output path khác nhau.
  • Dùng flag --bin chọn binary cụ thể trong project có nhiều bin, --example chạy demo trong examples/.
  • Bật/tắt feature compile-time bằng --features--no-default-features.
  • Có workflow dev khuyến nghị: cargo check liên tục khi gõ code → cargo run khi test logic → cargo build --release khi đo bench / deploy.
  • Cài cargo-watch để auto rerun mỗi khi save file.

Bài Bài 20: Cargo.toml Anatomy đã xem profile dev / release trong manifest. Bài này tập trung vào lệnh CLI hằng ngày.

2

cargo check — Kiểm Tra Nhanh Không Sinh Binary

cargo check chạy front-end của rustc: typecheck, borrow check, name resolution, macro expansion. Nó KHÔNG chạy code generation (LLVM IR → object), KHÔNG link. Kết quả là không có executable; chỉ sinh metadata .rmeta + một số .rlib metadata trong target/debug/deps/ cùng với .cargo-lock.

Tại sao nhanh hơn build 2-3 lần? Vì 60-70% thời gian compile Rust nằm ở phase codegen + LTO + link. Bỏ qua chính những phase đó, check chỉ làm phần phân tích syntax/semantics.

$ time cargo check
    Checking my-app v0.1.0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.85s

real    0m0.92s

Đây cũng chính là lệnh mà rust-analyzer chạy ngầm liên tục trong IDE để báo lỗi real-time. Mỗi lần bạn save file, rust-analyzer phát hành một incremental cargo check để cập nhật diagnostic — đó là lý do red squiggle xuất hiện trong vài trăm millisecond chứ không phải vài giây.

Khi nào dùng: gõ code và muốn biết "có lỗi compile không?". Workflow lý tưởng là check liên tục, chỉ chuyển sang build/run khi đã sẵn sàng test logic thực tế.

Pitfall thường gặp: code pass cargo check không có nghĩa là chạy được — vì linker error (thiếu symbol, mismatched ABI) chỉ phát hiện ở phase link mà check bỏ qua. Tuy hiếm, vẫn nên chạy ít nhất 1 lần cargo build trước khi commit.

3

cargo build — Compile Thực Sự

cargo build chạy đầy đủ pipeline: parse → typecheck → borrow check → MIR → LLVM IR → object → link ra binary cuối. Mặc định dùng profile dev (opt-level = 0, full debug info), output ở target/debug/<package-name>.

$ time cargo build
   Compiling my-app v0.1.0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.41s

real    0m2.48s

$ ls target/debug/
build/  deps/  examples/  incremental/  my-app  my-app.d

So sánh với cargo check 0.85s, cargo build mất 2.41s — gấp khoảng 3 lần do thêm codegen + link.

Sau khi build xong, executable ở target/debug/my-app có thể chạy độc lập mà không cần Cargo:

$ ./target/debug/my-app
Hello, world!

Incremental compilation: lần build thứ 2 trở đi (không đổi code) gần như zero-cost — Cargo cache fingerprint của từng crate, chỉ recompile crate có thay đổi. Đổi 1 file trong crate chính: rebuild crate đó. Đổi file ở dependency: rebuild dependency đó + tất cả crate phụ thuộc (cascade).

$ time cargo build
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.05s

real    0m0.09s

Khi nào dùng: muốn test binary thật, copy binary đi nơi khác, hoặc debug với debugger (lldb/gdb) cần symbol table đầy đủ — profile dev giữ nguyên debug info nên trải nghiệm debug rất mượt.

4

cargo run — Build + Execute Trong 1 Lệnh

cargo run thực hiện cargo build (nếu cần) rồi exec binary ngay. Lệnh phổ biến nhất khi dev: tiết kiệm 2 step thành 1.

$ cargo run
   Compiling my-app v0.1.0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.41s
     Running `target/debug/my-app`
Hello, world!

Truyền argument cho binary: chú ý phải có -- để Cargo biết các flag sau đó thuộc về binary chứ không phải Cargo:

$ cargo run -- arg1 arg2
     Running `target/debug/my-app arg1 arg2`
Got args: ["arg1", "arg2"]

# Sai: --port sẽ bị Cargo cố gắng parse và báo lỗi unknown flag
$ cargo run --port 8080
error: unexpected argument '--port' found

# Đúng: --  ngăn cách rõ ràng
$ cargo run -- --port 8080
     Running `target/debug/my-app --port 8080`

Ưu điểm: dev loop ngắn — sửa code → cargo run → xem output → sửa tiếp. Không cần nhớ path target/debug/<name>.

Nhược điểm: mỗi lần chạy phải re-link (vài trăm ms ở project nhỏ, vài giây ở project lớn). Nếu chỉ muốn kiểm tra compile có pass không, dùng cargo check nhanh hơn nhiều.

cargo run cũng chấp nhận --release, --bin, --example, --features giống cargo build.

5

cargo build --release — Optimized Build

--release chuyển từ profile dev sang profile release: opt-level = 3, không debug info, có thể bật LTO. Binary nhỏ và nhanh hơn dev build từ 10 đến 50 lần tùy workload — nhưng build chậm hơn nhiều do LLVM phải tối ưu rất kỹ.

$ time cargo build --release
   Compiling my-app v0.1.0
    Finished `release` profile [optimized] target(s) in 18.92s

real    0m18.99s

$ ls target/release/
build/  deps/  examples/  incremental/  my-app  my-app.d

Output ở target/release/<name> (khác target/debug/). Cargo giữ cả 2 thư mục song song để không phải rebuild khi switch profile.

$ ls -lh target/debug/my-app target/release/my-app
-rwxr-xr-x  4.2M  target/debug/my-app
-rwxr-xr-x  412K  target/release/my-app

Quy tắc: KHÔNG dùng release cho dev hằng ngày. Build 19s mỗi lần thay đổi sẽ phá nát workflow. Release chỉ phù hợp khi:

  • Benchmark: đo perf phải dùng binary tối ưu, không thì số liệu vô nghĩa (dev có thể chậm gấp 50 lần).
  • Deploy production: container, package binary cho user.
  • Profile / flamegraph: cần code gần với production thực tế.

Pitfall: cargo run --release ở project lớn cũng phải build lại nếu thư mục target/release/ stale — chuẩn bị tinh thần chờ.

6

Flag --bin Cho Multiple Binary

Project có thể có nhiều binary: file src/main.rs là bin mặc định cùng tên package, và mỗi file trong src/bin/*.rs là một bin riêng (tên = tên file). Hoặc khai báo tường minh qua [[bin]] trong Cargo.toml (xem Bài 20).

$ tree src
src
├── main.rs        # bin tên "my-app"
└── bin
    ├── server.rs  # bin tên "server"
    └── worker.rs  # bin tên "worker"

Khi có nhiều bin, cargo run không biết chọn cái nào — phải chỉ định --bin:

# Build riêng bin "server"
$ cargo build --bin server

# Run bin "worker" và truyền arg cho nó
$ cargo run --bin worker -- --port 8080
     Running `target/debug/worker --port 8080`

# Run bin "worker" ở release profile
$ cargo run --bin worker --release -- --port 8080
     Running `target/release/worker --port 8080`

Idiom phổ biến trong project production: tách server (HTTP API), worker (background job), migrate (CLI chạy DB migration), cli (admin tool) thành nhiều bin chia sẻ chung library src/lib.rs. Cargo build chỉ những bin cần thiết khi specify --bin, tiết kiệm thời gian.

7

Flag --example Cho Example Programs

Thư mục examples/ dành cho chương trình demo — mỗi file .rs là một example độc lập, có fn main, dùng API public của crate để minh hoạ cách dùng. Khác bin: example KHÔNG vào binary chính khi cargo build mặc định, KHÔNG được publish lên crates.io binary, chỉ build khi gọi tên cụ thể.

$ tree examples
examples
├── basic.rs
├── smoke.rs
└── load_test.rs

# Build và chạy example "smoke"
$ cargo run --example smoke
   Compiling my-app v0.1.0
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.87s
     Running `target/debug/examples/smoke`

# Chỉ build, không chạy
$ cargo build --example smoke

# Build tất cả example
$ cargo build --examples

Use case: library crate (Axum, sqlx, tokio) đều có thư mục examples/ để người dùng tham khảo nhanh. cargo run --example basic nhân bản scenario chính chỉ trong vài giây, không cần tạo project test riêng.

Output binary nằm ở target/debug/examples/<name> (hoặc target/release/examples/<name> khi kèm --release).

8

Flag --features Bật Feature

Feature là cờ compile-time định nghĩa trong [features] của Cargo.toml (đã học ở Bài 20). Khi build, dùng --features để bật một hoặc nhiều feature:

# Bật 2 feature cùng lúc (cách nhau bằng dấu phẩy)
$ cargo build --features cache,redis

# Bật toàn bộ feature có thể (test compile mọi tổ hợp)
$ cargo build --all-features

# Tắt default feature, chỉ bật cái mình cần
$ cargo build --no-default-features --features json

# Run kèm feature
$ cargo run --features jwt -- --port 8080

--no-default-features đặc biệt hữu ích khi muốn binary nhỏ nhất có thể — tắt mọi feature mặc định mà mình không dùng, giảm code size đáng kể.

Pitfall: gõ sai tên feature, Cargo không báo lỗi ngay mà chỉ ghi warning nhỏ — chú ý đọc kỹ output dòng đầu tiên.

Combo thực dụng cho CI: chạy build matrix với --no-default-features, từng feature riêng, và --all-features để đảm bảo mọi #[cfg(feature = "x")] đều compile sạch.

9

Workflow Khuyến Nghị

Tổng hợp lại thành workflow hằng ngày:

  • Khi đang gõ code: cargo check (hoặc để rust-analyzer làm hộ trong IDE). Nhanh, biết ngay có lỗi compile không.
  • Khi cần test logic thực: cargo run --bin foo hoặc cargo test để chạy thử behavior.
  • Khi cần đo perf hoặc deploy: cargo build --release. Đừng dùng dev binary để benchmark.
  • Khi có nhiều variant feature: cargo build --no-default-features --features minimal để kiểm tra binary lean.

Tip mạnh: cài cargo-watch để Cargo tự rerun mỗi lần file thay đổi — không phải gõ lệnh lại liên tục:

$ cargo install cargo-watch && cargo watch -x check -x test -x run

Lệnh trên sẽ: chạy cargo check trước (fail fast), sau đó cargo test, cuối cùng cargo run — mọi lúc bạn save một file .rs trong project. Có thể tinh chỉnh:

# Chỉ check liên tục (nhẹ nhất, dùng khi gõ refactor lớn)
$ cargo watch -x check

# Check + test, bỏ qua run
$ cargo watch -x check -x test

# Chạy server, kill và restart khi save
$ cargo watch -x 'run --bin server'

Kết hợp với rust-analyzer trong VS Code, dev loop Rust trở nên rất gần với JS/TS: save → thấy kết quả trong vài giây.

10

Tổng Kết

  • cargo check: typecheck + borrow check, KHÔNG link, KHÔNG sinh binary. Nhanh gấp 2-3 lần build. Lệnh mà rust-analyzer chạy ngầm liên tục.
  • cargo build: compile + link, output target/debug/<name>. Default profile dev với debug info đầy đủ. Có incremental compilation.
  • cargo run: gộp build (nếu cần) + execute. Dùng -- để truyền argument cho binary tránh xung đột với flag Cargo.
  • cargo build --release: profile release, opt-level 3, binary nhanh 10-50x nhưng build chậm hơn nhiều. Chỉ dùng cho bench/deploy, KHÔNG dev hằng ngày. Output target/release/<name>.
  • --bin <name>: chọn binary cụ thể khi project có nhiều bin trong src/bin/ hoặc [[bin]].
  • --example <name>: build và chạy file trong examples/ — demo cách dùng crate, không vào binary chính.
  • --features a,b bật feature; --no-default-features tắt default; --all-features bật hết để test compile matrix.
  • Workflow: cargo check khi gõ → cargo run khi test → cargo build --release khi bench/deploy. Cài cargo-watch để auto rerun on save.
11

Bài Tập Củng Cố

Tự trả lời, đáp án ở cuối:

  1. Bạn vừa sửa 50 dòng code và muốn biết "có lỗi compile không?" mà chưa cần chạy thử. Lệnh nào tiết kiệm nhất, và nó bỏ qua phase nào của rustc?
  2. Đồng nghiệp than phiền cargo run --port 8080 báo lỗi "unexpected argument '--port'". Lệnh đúng phải là gì? Vì sao?
  3. Project có src/main.rs, src/bin/server.rs, src/bin/worker.rs. Gõ cargo run trần thì Cargo chọn binary nào? Nếu Cargo báo lỗi "could not determine which binary to run", phải chỉ định thế nào?
  4. Bạn vừa benchmark binary target/debug/my-app và thấy throughput chỉ 1000 req/s. Đồng nghiệp nói "đo sai rồi". Vì sao? Sửa thế nào?
  5. Crate có 3 feature cache, jwt, metrics; default = ["jwt"]. Bạn muốn build với CHỈ cache + metrics, không có jwt. Lệnh đầy đủ là gì?
Đáp án
  1. cargo check — bỏ qua phase codegen (MIR → LLVM IR → object) và phase link. Chỉ chạy parse, name resolution, macro expansion, typecheck, borrow check. Nhanh gấp 2-3 lần cargo build.
  2. cargo run -- --port 8080. Dấu -- ngăn cách flag của Cargo (trước nó) và argument của binary (sau nó). Không có --, Cargo cố parse --port như flag của chính nó và báo lỗi.
  3. Cargo sẽ ưu tiên src/main.rs (bin cùng tên package) khi có. Nếu xung đột hoặc package chỉ có lib + nhiều bin trong src/bin/, phải dùng cargo run --bin server hoặc cargo run --bin worker.
  4. Vì binary target/debug/ là dev profile, opt-level = 0, không inline, không tối ưu. Số throughput thường thấp hơn release 10-50 lần và hoàn toàn không phản ánh thực tế. Sửa: cargo build --release rồi chạy target/release/my-app, hoặc cargo run --release.
  5. cargo build --no-default-features --features cache,metrics. --no-default-features tắt jwt (vì nó nằm trong default), --features cache,metrics bật 2 feature cần.
12

Bài Tiếp Theo

Bài 22: Profile dev vs release — Debug Build Và Optimized Build — đi sâu vào từng field của [profile.dev][profile.release]: opt-level, lto, codegen-units, strip, panic, custom profile [profile.bench] kế thừa release, và cách giảm binary size từ 30 MB xuống còn 5-8 MB.