Danh sách bài viết

Bài 266: cargo publish — Đẩy Crate Lên crates.io

Bài 266 của series Rust Cơ Bản — sau hơn 260 bài chỉ dùng crate từ crates.io, đã đến lúc đi ngược chiều: contribute một crate của riêng bạn lên registry chính thức để cả thế giới Rust dùng được qua một dòng cargo add. Bài này đi qua quy trình đầy đủ: tạo account crates.io bằng GitHub OAuth và xác nhận email, lấy API token với cargo login, hoàn thiện 5 trường metadata bắt buộc trong Cargo.toml (name unique toàn registry, version, description, license theo SPDX identifier, repository), kiểm tra trước bằng cargo publish --dry-run để bắt thiếu sót sớm, chạy cargo publish đẩy lên thật, hiểu cách bump version đúng theo SemVer cho lần publish kế tiếp, phân biệt semantic version (cho cargo resolver) với marketing version (cho người đọc), và quan trọng nhất: nắm rule crates.io immutable — không có lệnh unpublish, chỉ có cargo yank để ngăn version mới dùng nhưng giữ lock file cũ tiếp tục build được.

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

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

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

  • Tạo được account crates.io qua GitHub OAuth, xác nhận email và sinh API token.
  • Lưu token an toàn vào ~/.cargo/credentials.toml bằng cargo 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 publish reject.
  • Dùng cargo publish --dry-run để xem packaging + validate metadata mà chưa upload thật.
  • Chạy cargo publish upload 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.io không có unpublish — chỉ có cargo yank --version X.Y.Z ngăn version mới dùng nhưng lock file cũ vẫn build được.
2

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:

  1. Truy cập https://crates.io, click nút Log in with GitHub ở góc phải trên cùng.
  2. 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.
  3. 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 publish sẽ báo lỗi A verified email address is required to publish crates.
  4. 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ằng cio — 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.

3

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.toml lê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 customize CARGO_HOME vào project dir.
  • Để pass token qua biến môi trường (CI): set CARGO_REGISTRY_TOKEN=cio_... trước khi chạy cargo 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 logout xoá entry trong credentials.toml.
4

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ặc cargo search <name>. Tên đụng → publish reject với crate 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, format MAJOR.MINOR.PATCH. Lần publish đầu thường 0.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ùng license-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
5

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:

  1. Packaging: Cargo tạo file .crate (tar.gz) trong target/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 .gitignore hoặc exclude trong Cargo.toml. Mở file để verify: tar tzf target/package/my-awesome-crate-0.1.0.crate.
  2. 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.rs reference file ngoài source dir), hoặc dep path = "..." mà không có version (không publish được path dep).
  3. Compiling: build thật, có thể mất thời gian với crate lớn.
  4. Uploading (bị skip với --dry-run): khi không có flag, sẽ thật sự upload .crate file 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-dirty nếu chắc chắn — nhưng best practice là commit hết trước khi publish để release tag khớp source.
6

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.

7

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.01.0.1): bugfix, không đổi API public. User chạy cargo update sẽ nhận bản này tự động.
  • MINOR bump (1.0.01.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.02.0.0): breaking change (xoá function, đổi signature, đổi trait, đổi behavior). User không nhận tự động — phải sửa Cargo.toml chủ động.

Pre-1.0 (0.x.y) có rule riêng: 0.MINOR đóng vai trò như major — bump 0.1.00.2.0 là breaking; chỉ 0.1.00.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, vd 1.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.21.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.

8

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-crate hoặc cargo update bỏ qua version đã yank — chọn version khác trong range.
  • Lock file cũ vẫn build được: nếu project nào đó đã có Cargo.lock pin 0.1.0, build tiếp tục hoạt động bình thường. Yank không xoá file .crate trên registry, chỉ flag metadata.
  • Reversible: --undo khô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.

9

Tổng Kết

  • Account crates.io tạ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ường CARGO_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êm readme, keywords, categories, documentation.
  • cargo publish --dry-run simulate đầy đủ (package → verify → compile → skip upload). Luôn chạy trước thật.
  • cargo publish upload và register lên index. Sau ~30s tới 2 phút crate available qua cargo 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.io immutable — 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. --undo khôi phục được. Không phải security tool.
10

Bài Tập Củng Cố

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

  1. Bạn chạy cargo publish lần đầu nhưng nhận lỗi missing 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ị license idiom trong Rust ecosystem.
  2. 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 job publish ra sao?
  3. Crate my-tool hiện ở version 1.4.2. Bạn thêm function mới fn parse_v2(...) nhưng giữ nguyên fn parse(...) cũ. Bump version nào? Nếu thay vào đó đổi fn parse(s: &str) thành fn parse(s: &str, opts: Options) thì bump cái nào? Giải thích.
  4. Sau publish my-tool 0.5.0, bạn phát hiện file .env bị 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?
  5. So sánh cargo yank --version 1.0.0 vớ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?
  6. 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
  1. license nhận SPDX identifier text (MIT, Apache-2.0, MIT OR Apache-2.0, GPL-3.0-or-later); license-file trỏ 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).
  2. Dùng biến môi trường CARGO_REGISTRY_TOKEN thay vì commit credentials.toml hay chạy cargo 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ạy cargo publish.
  3. Thêm function mới giữ cái cũ → MINOR bump (1.4.21.5.0) vì API thêm backward-compatible. Đổi signature fn parse(s) thành fn parse(s, opts)MAJOR bump (1.4.22.0.0) vì code user gọi parse(s) sẽ không compile. Workaround không bump major: giữ fn parse(s) cũ + thêm fn 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.
  4. 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) publish 0.5.1 sạ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êm exclude = [".env", "*.key", "secrets/"] trong [package], và luôn chạy cargo publish --dry-run kèm tar tzf target/package/*.crate để review file list.
  5. 3 khác biệt: (a) Yank không xoá file — project nào đã có Cargo.lock pin 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.0 exact; 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.
  6. (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.toml tồn tại. (5) Trong project: đảm bảo Cargo.toml đủ 5 field bắt buộc (name unique, version 0.1.0, description, license SPDX, repository URL). (6) cargo search my-first-crate verify 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 publish thật. (11) Verify trên https://crates.io/crates/my-first-crate sau ~30s; check docs.rs build sau ~5 phút.
11

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.