Mục lục
Mục Tiêu Bài Học
Sau bài học, bạn sẽ:
- Tạo được account
crates.ioqua GitHub OAuth, xác nhận email và sinh API token. - Lưu token an toàn vào
~/.cargo/credentials.tomlbằngcargo login. - Biết 5 trường metadata bắt buộc trong
[package]trước khi publish:name,version,description,license(SPDX),repository— thiếu làcargo publishreject. - Dùng
cargo publish --dry-runđể xem packaging + validate metadata mà chưa upload thật. - Chạy
cargo publishupload lần đầu, hiểu output từng giai đoạn (verify → package → upload → index). - Bump version đúng SemVer cho lần publish kế tiếp: patch (1.0.0 → 1.0.1) cho bugfix, minor (1.0.0 → 1.1.0) cho feature backward-compatible, major (1.0.0 → 2.0.0) cho breaking change.
- Phân biệt semantic version (Cargo resolver dùng) với marketing version (con người đọc) và dùng đúng cho từng mục đích.
- Hiểu vì sao
crates.iokhông có unpublish — chỉ cócargo yank --version X.Y.Zngăn version mới dùng nhưng lock file cũ vẫn build được.
Tạo Account crates.io + GitHub OAuth
crates.io không có form đăng ký username/password riêng — đăng nhập qua GitHub OAuth là cách duy nhất. Nguyên do lịch sử: Rust Foundation muốn giảm bề mặt tấn công (không quản lý password) và đảm bảo mỗi crate gắn được với identity public đã có reputation.
Các bước:
- Truy cập https://crates.io, click nút Log in with GitHub ở góc phải trên cùng.
- GitHub hiện màn hình authorize app
rust-lang/crates.io— accept. Lần đầu, GitHub tạo session OAuth, sau đó không hỏi lại trừ khi revoke. - Crates.io redirect về dashboard, prompt nhập email — bắt buộc (dùng để gửi thông báo bảo mật, ownership transfer, takeover request). Nhập xong, kiểm tra inbox và click link xác nhận. Trước khi xác nhận,
cargo publishsẽ báo lỗiA verified email address is required to publish crates. - Vào Account Settings → API Tokens, click New Token. Đặt tên gợi nhớ (vd
laptop-publish) và chọn scope. Mặc định token có full quyền (publish, yank, owner management). Token bắt đầu bằngcio— copy ngay vì màn hình chỉ hiện một lần.
Tip bảo mật: tạo nhiều token với scope hẹp hơn là dùng chung một token. Vd token CI chỉ cần publish-new + publish-update, không cần yank hay change-owners — nếu CI bị lộ token, attacker không xoá được crate hay đổi owner.
cargo login <token>
Sau khi có token, lưu vào local config bằng:
$ cargo login cio_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789
Login token for `crates-io` saved
Please make sure to not share your token with anyone.
Logging in from a public computer? Consider running `cargo logout`.
Cargo ghi token vào file ~/.cargo/credentials.toml (Windows: %USERPROFILE%\.cargo\credentials.toml) với permission 0600 chỉ user đọc được. Nội dung file:
# ~/.cargo/credentials.toml
[registry]
token = "cio_aBcDeFgHiJkLmNoPqRsTuVwXyZ0123456789"
Một số điểm quan trọng:
- Không commit file
credentials.tomllên git. Mặc định nó nằm trong home dir nên không bị repo kéo, nhưng cẩn thận nếu bạn customizeCARGO_HOMEvào project dir. - Để pass token qua biến môi trường (CI): set
CARGO_REGISTRY_TOKEN=cio_...trước khi chạycargo publish. Cargo tự đọc, không cần file credentials. Cách này khuyến nghị cho GitHub Actions, GitLab CI, CircleCI. - Mất token / nghi ngờ lộ → vào https://crates.io/me revoke token cũ, tạo mới, chạy lại
cargo login. - Đăng xuất máy public:
cargo logoutxoá entry trongcredentials.toml.
Metadata Bắt Buộc Trong Cargo.toml
Trước khi publish, Cargo.toml phải có đủ 5 trường sau trong section [package]:
[package]
name = "my-awesome-crate" # Unique trên toàn crates.io — first come first serve
version = "0.1.0" # SemVer, sẽ bump theo rule
edition = "2024"
description = "Một dòng ngắn gọn về crate (max 200 char, hiển thị trên search result)"
license = "MIT OR Apache-2.0" # SPDX identifier — KHÔNG tự đặt tên license
repository = "https://github.com/yourname/my-awesome-crate"
# Khuyến nghị mạnh (không bắt buộc nhưng nên có)
readme = "README.md" # Cargo tự đọc và hiện trên trang crate
keywords = ["cli", "json", "parser"] # Tối đa 5, dùng cho search
categories = ["command-line-utilities", "parsing"] # Từ list cố định của crates.io
documentation = "https://docs.rs/my-awesome-crate" # docs.rs tự build, dùng URL này
homepage = "https://my-awesome-crate.dev"
Giải thích từng trường bắt buộc:
name: unique toàn registry, first-come first-serve. Check trước trên crates.io hoặccargo search <name>. Tên đụng → publish reject vớicrate name "x" is already taken. Tên nên ngắn, snake_case hoặc kebab-case, không dùng tên gây nhầm với crate phổ biến (tránh typosquatting).version: tuân thủ SemVer 2.0, formatMAJOR.MINOR.PATCH. Lần publish đầu thường0.1.0(báo hiệu pre-1.0 chưa stable). Cùng version không publish lại được — phải bump.description: 1-2 câu, hiện trên search result và trang crate. Tránh viết "A Rust library for..." (lặp ngữ cảnh) — đi thẳng vào value proposition.license: SPDX identifier. Phổ biến nhất:MIT,Apache-2.0,MIT OR Apache-2.0(dual-license, idiom Rust ecosystem). Nếu license non-standard, dùnglicense-file = "LICENSE"trỏ tới file thay vìlicense. Không có cả hai → publish reject.repository: URL public tới source code (GitHub, GitLab, codeberg...). Bắt buộc để user verify code và contribute. Nếu chưa có repo public, tạo trước.
Lỗi điển hình khi thiếu license:
$ cargo publish
error: failed to publish to registry at https://crates.io
Caused by:
the remote server responded with an error: missing or empty metadata fields: license, license_file.
Please see https://doc.rust-lang.org/cargo/reference/manifest.html for more information on configuring these fields
cargo publish --dry-run Kiểm Tra Local
Trước khi đẩy thật, luôn chạy --dry-run để Cargo simulate toàn bộ quy trình mà không upload:
$ cargo publish --dry-run
Updating crates.io index
Packaging my-awesome-crate v0.1.0 (/Users/you/projects/my-awesome-crate)
Verifying my-awesome-crate v0.1.0 (/Users/you/projects/my-awesome-crate)
Compiling my-awesome-crate v0.1.0 (/Users/you/projects/my-awesome-crate/target/package/my-awesome-crate-0.1.0)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.34s
Packaged 12 files, 18.4KiB (5.2KiB compressed)
Uploading my-awesome-crate v0.1.0 (/Users/you/projects/my-awesome-crate)
warning: aborting upload due to dry run
Bốn giai đoạn:
- Packaging: Cargo tạo file
.crate(tar.gz) trongtarget/package/. Liệt kê các file sẽ include — mặc định tất cả file trong git tracking, trừ những file nằm trong.gitignorehoặcexcludetrongCargo.toml. Mở file để verify:tar tzf target/package/my-awesome-crate-0.1.0.crate. - Verifying: Cargo extract package vừa tạo vào thư mục tạm và build lại từ đó. Bước này phát hiện thiếu file (vd
build.rsreference file ngoài source dir), hoặc deppath = "..."mà không có version (không publish được path dep). - Compiling: build thật, có thể mất thời gian với crate lớn.
- Uploading (bị skip với
--dry-run): khi không có flag, sẽ thật sự upload.cratefile lên crates.io.
Các vấn đề --dry-run bắt được:
- Thiếu metadata bắt buộc (license, description...).
- Dep dùng
path = "..."không cóversion = "..."kèm — registry không cho phép path dep vì user khác không có path đó. Fix: thêm version, hoặc đẩy dep đó lên registry trước. - File quá lớn (default limit 10 MB cho crate file). Fix: thêm
exclude = ["assets/big-file"]trong[package]. - Working directory có uncommitted changes (default Cargo cảnh báo). Force qua
--allow-dirtynếu chắc chắn — nhưng best practice là commit hết trước khi publish để release tag khớp source.
cargo publish Đẩy Lên
Khi --dry-run pass và git working tree đã commit, chạy:
$ cargo publish
Updating crates.io index
Packaging my-awesome-crate v0.1.0
Verifying my-awesome-crate v0.1.0
Compiling my-awesome-crate v0.1.0
Finished `dev` profile [unoptimized + debuginfo] target(s) in 2.41s
Packaged 12 files, 18.4KiB (5.2KiB compressed)
Uploading my-awesome-crate v0.1.0
Uploaded my-awesome-crate v0.1.0 to registry `crates-io`
note: waiting for `my-awesome-crate v0.1.0` to be available at registry `crates-io`.
You may press ctrl-c to skip waiting; the crate should be available shortly.
Published my-awesome-crate v0.1.0 at registry `crates-io`
Sau khoảng 30 giây tới 2 phút, crate xuất hiện trên https://crates.io/crates/my-awesome-crate. docs.rs tự động build doc trong background và publish tại https://docs.rs/my-awesome-crate/0.1.0.
Lúc này bất kỳ ai cũng có thể dùng crate của bạn bằng:
$ cargo add my-awesome-crate
Updating crates.io index
Adding my-awesome-crate v0.1.0 to dependencies
Lưu ý quan trọng: không có nút undo trong vòng 5 phút. Crates.io không có grace period — vừa upload xong là vĩnh viễn. Nếu publish nhầm, không thể xoá, chỉ có thể yank (sẽ bàn ở mục 8). Đó là lý do --dry-run không phải optional.
Version Bump + Semantic vs Marketing Version
Lần publish thứ hai trở đi, phải bump version trong Cargo.toml — Cargo không cho phép overwrite version đã tồn tại. Quy tắc bump theo SemVer 2.0:
- PATCH bump (
1.0.0→1.0.1): bugfix, không đổi API public. User chạycargo updatesẽ nhận bản này tự động. - MINOR bump (
1.0.0→1.1.0): thêm API mới backward-compatible (thêm function, thêm trait method có default impl, thêm enum variant non-exhaustive). User cũng nhận tự động. - MAJOR bump (
1.0.0→2.0.0): breaking change (xoá function, đổi signature, đổi trait, đổi behavior). User không nhận tự động — phải sửaCargo.tomlchủ động.
Pre-1.0 (0.x.y) có rule riêng: 0.MINOR đóng vai trò như major — bump 0.1.0 → 0.2.0 là breaking; chỉ 0.1.0 → 0.1.1 là backward-compatible. Khi crate đủ stable, bump lên 1.0.0 để ký commitment với user.
Phân biệt 2 khái niệm version dễ lẫn:
- Semantic version (con số trong
Cargo.toml, vd1.4.2): dành cho Cargo resolver. Mỗi số đều có ý nghĩa kỹ thuật chính xác về compatibility. Không phải để marketing. - Marketing version (vd "Tokio 1.0 Edition", "Rust 2024 Edition"): dành cho con người — kể câu chuyện, đánh dấu cột mốc. Có thể không khớp semantic version. Vd Tokio bump từ 0.3 lên 1.0 vì commitment API ổn định (marketing), đồng thời cũng là breaking change so với 0.3.x (semantic).
Đừng delay bump major chỉ vì sợ "version số to". Sửa breaking change nhưng giữ minor (vd 1.4.2 → 1.5.0) phá user — họ nhận update qua cargo update và build vỡ không lý do. Đúng SemVer → user trust crate hơn, ecosystem health hơn.
cargo yank — Không Có Unpublish
Sự thật quan trọng nhất về crates.io: không có lệnh unpublish. Một khi crate đã được upload, nó tồn tại vĩnh viễn (trừ trường hợp cực hiếm như rò rỉ thông tin nhạy cảm hoặc copyright violation — phải email team [email protected] để xử lý thủ công). Lý do: nếu cho phép xoá, các project đang depend vào version đó sẽ vỡ build đột ngột (giống thảm hoạ left-pad trên npm năm 2016).
Thay vào đó, có cargo yank:
# Yank version 0.1.0 — đánh dấu "không nên dùng nữa"
$ cargo yank --version 0.1.0
Updating crates.io index
Yank [email protected]
# Khôi phục version đã yank (nếu yank nhầm)
$ cargo yank --version 0.1.0 --undo
Updating crates.io index
Unyank [email protected]
Hành vi của yank:
- Resolve mới không chọn: lệnh
cargo add my-awesome-cratehoặccargo updatebỏ qua version đã yank — chọn version khác trong range. - Lock file cũ vẫn build được: nếu project nào đó đã có
Cargo.lockpin0.1.0, build tiếp tục hoạt động bình thường. Yank không xoá file.cratetrên registry, chỉ flag metadata. - Reversible:
--undokhôi phục — khác hẳn unpublish (nếu có) sẽ xoá vĩnh viễn. - Không phải security tool: yank không ngăn user cố tình chỉ định
my-awesome-crate = "=0.1.0"(exact) — họ vẫn tải được. Nếu có vulnerability nghiêm trọng, yank + đăng advisory trên RustSec + push bản fix với version mới.
Khi nào yank: phát hiện bug nghiêm trọng (security, data corruption, panic chắc chắn), build bị vỡ do typo trong Cargo.toml, lỡ publish placeholder code. Không yank vì "có version mới tốt hơn" — đó là vai trò của SemVer + bump version.
Tổng Kết
- Account
crates.iotạo qua GitHub OAuth + verified email. API token sinh ở Account Settings, copy ngay vì chỉ hiện một lần. cargo login <token>lưu token vào~/.cargo/credentials.toml(mode 0600). CI dùng biến môi trườngCARGO_REGISTRY_TOKEN.- 5 trường
[package]bắt buộc:name(unique, first-come),version(SemVer),description,license(SPDX identifier nhưMIT OR Apache-2.0),repository. Khuyến nghị thêmreadme,keywords,categories,documentation. cargo publish --dry-runsimulate đầy đủ (package → verify → compile → skip upload). Luôn chạy trước thật.cargo publishupload và register lên index. Sau ~30s tới 2 phút crate available quacargo add. docs.rs tự build doc.- Version bump theo SemVer: PATCH cho bugfix, MINOR cho feature backward-compat, MAJOR cho breaking. Pre-1.0 thì
0.MINORđóng vai trò major. - Semantic version (cho resolver) khác marketing version (cho con người). Đừng delay major bump vì sợ "số to" — phá user còn tệ hơn.
crates.ioimmutable — không có unpublish.cargo yank --version X.Y.Zđánh dấu version "không nên dùng mới"; lock file cũ vẫn build.--undokhôi phục được. Không phải security tool.
Bài Tập Củng Cố
Tự trả lời, đáp án ở cuối:
- Bạn chạy
cargo publishlần đầu nhưng nhận lỗimissing or empty metadata fields: license, license_file. Trường này khác gìlicense-file, khi nào dùng cái nào? Cho ví dụ giá trịlicenseidiom trong Rust ecosystem. - Trong CI GitHub Actions, không nên dùng
cargo login <token>mà nên dùng cách khác. Cách đó là gì? Vì sao tốt hơn? Đoạn YAML mẫu set token cho jobpublishra sao? - Crate
my-toolhiện ở version1.4.2. Bạn thêm function mớifn parse_v2(...)nhưng giữ nguyênfn parse(...)cũ. Bump version nào? Nếu thay vào đó đổifn parse(s: &str)thànhfn parse(s: &str, opts: Options)thì bump cái nào? Giải thích. - Sau publish
my-tool 0.5.0, bạn phát hiện file.envbị accidentally include trong package (có credentials). Yank giải quyết được không? Quy trình đúng để xử lý là gì? Có thể "ngừa" cho lần sau bằng cách nào? - So sánh
cargo yank --version 1.0.0với (giả tưởng)cargo unpublish --version 1.0.0: 3 điểm khác biệt về hành vi với project downstream đang dùng version đó. Vì sao Rust Foundation chọn yank thay vì unpublish? - Mô tả tuần tự từng bước từ lúc bạn chưa có account crates.io tới lúc publish thành công
my-first-crate 0.1.0. Liệt kê command + việc kiểm tra ở mỗi bước.
Đáp án
licensenhận SPDX identifier text (MIT,Apache-2.0,MIT OR Apache-2.0,GPL-3.0-or-later);license-filetrỏ tới file trong repo chứa nội dung license non-standard hoặc proprietary. Phải có ít nhất một trong hai. Idiom Rust:license = "MIT OR Apache-2.0"(dual-license, để consumer chọn license phù hợp với project của họ — pattern được Rust project chính và phần lớn ecosystem dùng).- Dùng biến môi trường
CARGO_REGISTRY_TOKENthay vì commitcredentials.tomlhay chạycargo login(sẽ ghi file vào runner). Tốt hơn vì: (a) không để lại artifact secrets trên disk, (b) GitHub Secrets quản lý token tập trung, (c) revoke nhanh khi cần. YAML mẫu:env: { CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }} }trong step chạycargo publish. - Thêm function mới giữ cái cũ → MINOR bump (
1.4.2→1.5.0) vì API thêm backward-compatible. Đổi signaturefn parse(s)thànhfn parse(s, opts)→ MAJOR bump (1.4.2→2.0.0) vì code user gọiparse(s)sẽ không compile. Workaround không bump major: giữfn parse(s)cũ + thêmfn parse_with_opts(s, opts), hoặc dùng default arg trick qua builder — nhưng API "chính xác hơn" thường đáng bump major. - Yank ngăn user mới dùng version đó, nhưng không xoá file, ai cũng vẫn tải được. Credentials bị lộ rồi → assume compromised: (1) revoke/rotate credential ngay lập tức (đổi DB password, API key...), (2)
cargo yank --version 0.5.0để ngừa user mới install, (3) publish0.5.1sạch sẽ, (4) email[email protected]giải thích tình huống — họ có thể xoá file trong trường hợp leak sensitive data thật sự. Ngừa lần sau: thêmexclude = [".env", "*.key", "secrets/"]trong[package], và luôn chạycargo publish --dry-runkèmtar tzf target/package/*.crateđể review file list. - 3 khác biệt: (a) Yank không xoá file — project nào đã có
Cargo.lockpin version đó vẫn build OK; unpublish sẽ vỡ build ngay khi clone fresh. (b) Yank reversible bằng--undo; unpublish vĩnh viễn. (c) Yank vẫn cho user cố tình tải bằng=1.0.0exact; unpublish chặn hoàn toàn. Foundation chọn yank vì ưu tiên ecosystem stability: thảm hoạ left-pad npm 2016 (developer unpublish crate 11 dòng, hàng nghìn project vỡ build trong vài giờ) là bài học không lặp lại. Yank cho phép "soft deprecate" mà không phá ai. - (1) Truy cập crates.io, login GitHub OAuth, accept authorize. (2) Nhập email + click link verify trong inbox. (3) Account Settings → API Tokens → New Token (đặt tên + scope) → copy token
cio_.... (4) Local:cargo login cio_..., verify file~/.cargo/credentials.tomltồn tại. (5) Trong project: đảm bảoCargo.tomlđủ 5 field bắt buộc (name unique, version0.1.0, description, license SPDX, repository URL). (6)cargo search my-first-crateverify tên chưa bị take. (7) Commit hết git working tree. (8)cargo publish --dry-run, đọc output kiểm tra file list, lỗi metadata. (9) Fix nếu có, dry-run lại. (10)cargo publishthật. (11) Verify trênhttps://crates.io/crates/my-first-cratesau ~30s; check docs.rs build sau ~5 phút.
Bài Tiếp Theo
Bài 267: cargo install — Cài Binary Từ Source — bài tiếp sang hướng ngược lại: tiêu thụ binary từ crates.io thay vì publish. Học cách dùng cargo install <crate> tải source, build và copy executable vào ~/.cargo/bin/; cargo install --git install thẳng từ git repo; --locked đảm bảo build reproducible từ Cargo.lock; cargo install-update cập nhật hàng loạt. Use case: cài CLI tool Rust (ripgrep, fd-find, bat, tokei) mà không cần package manager OS.
