Rust — Artigo #39
Gerenciamento de Dependências e Publicação de Crates — Do Projeto Local ao crates.io
Por Prof. Dr. Marcelo Fontes | Série: Dominando Rust em 1 Ano
Toda biblioteca que você escreveu nos artigos anteriores pode beneficiar outros desenvolvedores. O ecossistema Rust é construído por contribuições da comunidade — as mais de 150 mil crates disponíveis no crates.io foram criadas por pessoas que resolveram um problema e decidiram compartilhar a solução.
Neste artigo cobrimos o ciclo completo: gerenciar dependências com precisão, entender versionamento semântico, preparar uma crate para publicação, e manter uma biblioteca ao longo do tempo com compatibilidade retroativa.
Anatomia completa do Cargo.toml
[package]
name = "minha-crate" # nome no crates.io (único, com hífens)
version = "1.2.3" # semver obrigatório
edition = "2021" # edição do Rust (2015, 2018, 2021)
authors = ["Ana Silva <ana@exemplo.com>"]
description = "Uma linha descrevendo o que a crate faz"
documentation = "https://docs.rs/minha-crate"
homepage = "https://github.com/usuario/minha-crate"
repository = "https://github.com/usuario/minha-crate"
readme = "README.md"
license = "MIT OR Apache-2.0" # licença SPDX — dupla licença é convencional em Rust
keywords = ["parsing", "texto", "unicode"] # até 5, para busca no crates.io
categories = ["text-processing", "parsing"] # categorias do crates.io
exclude = ["tests/fixtures/", "benches/dados/"] # exclui do pacote publicado
include = ["src/**/*", "Cargo.toml", "README.md"] # alternativa ao exclude
# Versão mínima do Rust suportada (MSRV)
rust-version = "1.70"
[lib]
name = "minha_crate" # nome do crate (underscores)
path = "src/lib.rs" # caminho padrão, opcional
crate-type = ["lib"] # lib (padrão), cdylib, staticlib, proc-macro
[[bin]]
name = "minha-cli"
path = "src/bin/cli.rs"
[[example]]
name = "uso_basico"
path = "examples/basico.rs"
[[bench]]
name = "benchmark_principal"
path = "benches/principal.rs"
harness = false # para criterion
[[test]]
name = "integracao"
path = "tests/integracao.rs"
# Perfis de compilação
[profile.dev]
opt-level = 0
debug = true
incremental = true
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = "symbols" # remove símbolos de debug do binário
[profile.release-with-debug] # perfil customizado para profiling
inherits = "release"
debug = true
strip = "none"
[profile.bench]
inherits = "release"
debug = true
Especificando versões de dependências
O versionamento semântico (semver) é o contrato entre a sua crate e seus usuários:
[dependencies]
# Compatível com 1.x.x (^1.0 = >=1.0.0 <2.0.0) — padrão do Cargo
serde = "1"
serde = "1.0"
serde = "1.0.0"
serde = "^1.0.0" # explícito, mesmo que o anterior
# Compatível com patch (^0.8 = >=0.8.0 <0.9.0)
# Para 0.x, minor é breaking — semver especial
mio = "0.8"
# Compatível com qualquer patch (^0.0.3 = >=0.0.3 <0.0.4)
alguma-crate = "0.0.3"
# Tilde: compatível com patch, não minor
tokio = "~1.35" # >=1.35.0 <1.36.0
# Exato: fixado a uma versão específica (raramente adequado)
alguma-crate = "=1.2.3"
# Intervalo wildcard
alguma-crate = "1.*"
alguma-crate = "1.2.*"
# Intervalo explícito
alguma-crate = ">=1.2.0, <2.0.0"
# Múltiplas restrições
alguma-crate = ">= 1.0, < 2.0, != 1.3.0"
# Dependência de path (desenvolvimento local)
minha-lib = { path = "../minha-lib" }
# Dependência de git
experimental = { git = "https://github.com/usuario/repo" }
experimental = { git = "https://github.com/usuario/repo", branch = "main" }
experimental = { git = "https://github.com/usuario/repo", tag = "v1.2.3" }
experimental = { git = "https://github.com/usuario/repo", rev = "abc1234" }
# Com features
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"], default-features = false }
# Apenas para desenvolvimento
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
tempfile = "3"
proptest = "1"
rstest = "0.18"
# Apenas para build scripts
[build-dependencies]
cc = "1"
bindgen = "0.69"
Cargo.lock — reprodutibilidade
# Cargo.lock: gerado automaticamente, registra versões exatas
# Para BINÁRIOS: commitar o Cargo.lock no git
# → garante builds reprodutíveis para todos os colaboradores
# Para BIBLIOTECAS: NÃO commitar (convenção)
# → permite que usuários usem versões mais novas de dependências transitivas
# Verificar se as versões no lock são as usadas
cargo verify-project
# Atualizar dependências (respeitando restrições do Cargo.toml)
cargo update
# Atualizar uma dependência específica
cargo update -p serde
# Atualizar para a versão mais nova disponível
cargo update -p serde --precise 1.0.196
# Ver árvore de dependências
cargo tree
# Apenas dependências diretas
cargo tree --depth 1
# Encontrar por que uma crate está sendo incluída
cargo tree -i openssl
# Detectar versões duplicadas
cargo tree --duplicates
Workspaces — projetos multi-crate
Para projetos grandes, workspaces permitem múltiplas crates com configuração compartilhada:
# Cargo.toml da raiz do workspace
[workspace]
members = [
"core",
"cli",
"server",
"macros",
]
resolver = "2" # resolver de features (recomendado)
# Dependências compartilhadas — evita versões inconsistentes
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1"
thiserror = "1"
tracing = "0.1"
clap = { version = "4", features = ["derive"] }
[workspace.package]
version = "0.5.0" # versão compartilhada
edition = "2021"
license = "MIT OR Apache-2.0"
repository = "https://github.com/usuario/projeto"
# core/Cargo.toml
[package]
name = "meu-core"
version.workspace = true # herda do workspace
edition.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true # herda versão e features do workspace
anyhow.workspace = true
tokio.workspace = true
# Dependência específica deste crate
uuid = { version = "1", features = ["v4"] }
# cli/Cargo.toml
[package]
name = "meu-cli"
version.workspace = true
edition.workspace = true
[dependencies]
meu-core = { path = "../core" } # dependência interna
clap.workspace = true
anyhow.workspace = true
Preparando uma crate para publicação
1. Checklist de qualidade
# Verificar formatação
cargo fmt --check
# Rodar o linter
cargo clippy -- -D warnings
# Rodar todos os testes incluindo doc tests
cargo test --all-features
# Testar sem features padrão
cargo test --no-default-features
# Verificar documentação
cargo doc --all-features --no-deps
# Verificar se o pacote está correto
cargo package --list # lista arquivos incluídos
cargo package # gera o .crate sem publicar
2. README.md bem estruturado
# minha-crate
[](https://crates.io/crates/minha-crate)
[](https://docs.rs/minha-crate)
[](...)
[](...)
Uma linha descrevendo o que a crate faz.
## Uso
Adicione ao `Cargo.toml`:
[dependencies] minha-crate = "1"
Exemplo básico:
use minha_crate::MinhaStruct;
let s = MinhaStruct::nova("dados"); println!("{}", s.processar());
## Features
| Feature | Padrão | Descrição |
|---------|--------|-----------|
| `json` | ✓ | Suporte a JSON |
| `async` | | API assíncrona |
## MSRV
Rust 1.70 ou superior.
## Licença
MIT OR Apache-2.0
3. CI/CD com GitHub Actions
.github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
test:
name: Test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v4
- name: Instalar Rust
uses: dtolnay/rust-toolchain@stable
- name: Cache Cargo
uses: Swatinem/rust-cache@v2
- name: Build
run: cargo build --all-features
- name: Testes
run: cargo test --all-features
- name: Doc tests
run: cargo test --doc --all-features
msrv:
name: MSRV (Rust 1.70)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@1.70
- run: cargo build --all-features
linting:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Formato
run: cargo fmt --check
- name: Clippy
run: cargo clippy --all-features -- -D warnings
- name: Documentação
run: cargo doc --all-features --no-deps
env:
RUSTDOCFLAGS: "-D warnings"
security:
name: Auditoria de segurança
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: rustsec/audit-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
publish-dry-run:
name: Publicação (dry-run)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo publish --dry-run --all-features
Publicando no crates.io
# 1. Criar conta em https://crates.io
# 2. Gerar token de API em https://crates.io/settings/tokens
# 3. Autenticar localmente
cargo login <seu_token>
# 4. Verificar o pacote antes de publicar
cargo publish --dry-run
# 5. Publicar
cargo publish
# Publicar versão futura sem que seja a padrão de 'latest'
cargo publish --no-verify # pula verificação de arquivos
# Para workspace com múltiplos crates (ordem importa)
cargo publish -p meu-core
cargo publish -p meu-cli
Versionamento semântico na prática
MAJOR.MINOR.PATCH
PATCH (1.0.0 → 1.0.1): correção de bug sem mudança de API
MINOR (1.0.0 → 1.1.0): nova funcionalidade compatível com versões anteriores
MAJOR (1.0.0 → 2.0.0): mudança incompatível com versões anteriores
Para 0.x.y: MINOR pode ser breaking (0.1.0 → 0.2.0 = breaking)
O que é breaking change?
// BREAKING — requer nova versão MAJOR:
// ✗ Remover função pública
// ✗ Mudar assinatura de função (tipos de parâmetros ou retorno)
// ✗ Remover variante de enum público
// ✗ Remover campo de struct público
// ✗ Adicionar método obrigatório a trait público
// ✗ Mudar tipo de campo público
// ✗ Aumentar MSRV além do que a versão especifica
// NÃO breaking — pode ir em MINOR ou PATCH:
// ✓ Adicionar nova função pública
// ✓ Adicionar nova variante a enum #[non_exhaustive]
// ✓ Adicionar novo campo com valor padrão em struct #[non_exhaustive]
// ✓ Implementar novo trait para tipo existente
// ✓ Adicionar implementação de trait para tipo existente
// ✓ Adicionar método com implementação padrão a trait
// O atributo #[non_exhaustive] é seu aliado:
#[non_exhaustive] // previne match exaustivo externo
pub enum Evento {
Conectado,
Desconectado,
Erro(String),
// Podemos adicionar variantes em versões MINOR
}
#[non_exhaustive] // previne construção direta externa
pub struct Config {
pub host: String,
pub porta: u16,
// Podemos adicionar campos em versões MINOR
}
Depreciação e migração
// Depreciando uma API com guidance de migração
#[deprecated(
since = "2.1.0",
note = "Use `processar_v2()` que retorna Result em vez de Option.
Veja https://docs.rs/minha-crate/latest/minha_crate/fn.processar_v2.html"
)]
pub fn processar(dados: &str) -> Option<String> {
processar_v2(dados).ok()
}
pub fn processar_v2(dados: &str) -> Result<String, ErroProcessamento> {
if dados.is_empty() {
return Err(ErroProcessamento::EntradaVazia);
}
Ok(dados.to_uppercase())
}
#[derive(Debug)]
pub enum ErroProcessamento {
EntradaVazia,
}
// CHANGELOG.md bem mantido
CHANGELOG.md seguindo Keep a Changelog:
# Changelog
Todas as mudanças notáveis deste projeto são documentadas aqui.
Formato baseado em [Keep a Changelog](https://keepachangelog.com/pt-BR/1.1.0/).
## [Unreleased]
## [2.1.0] - 2024-03-15
### Adicionado
- `processar_v2()` retorna `Result` em vez de `Option` para melhor tratamento de erros
- Suporte a feature `yaml` para serialização YAML
### Depreciado
- `processar()` — use `processar_v2()`. Será removida na v3.0.
### Corrigido
- Corrigido panic ao processar strings com caracteres Unicode (#42)
## [2.0.0] - 2024-01-10
### Mudanças incompatíveis
- `Config` agora usa `#[non_exhaustive]` — construção direta requer `.default()`
- Removido `antigo_metodo()` (depreciado desde 1.5.0)
### Adicionado
- Suporte a ARM64 (Apple Silicon, Raspberry Pi)
## [1.5.0] - 2023-11-20
...
Um projeto completo: do zero ao crates.io
Vamos simular o ciclo completo de uma crate pequena mas bem feita:
minha-crate/
├── Cargo.toml
├── README.md
├── CHANGELOG.md
├── LICENSE-MIT
├── LICENSE-APACHE
├── .github/
│ └── workflows/
│ ├── ci.yml
│ └── release.yml
├── src/
│ ├── lib.rs
│ └── error.rs
├── tests/
│ └── integracao.rs
├── examples/
│ └── uso_basico.rs
└── benches/
└── principal.rs
src/lib.rs:
//! # slug-rs
//!
//! Converte texto em slugs URL-seguros.
//!
//! ## Uso
//!
//! ```rust
//! use slug_rs::slugify;
//!
//! assert_eq!(slugify("Olá, Mundo!"), "ola-mundo");
//! assert_eq!(slugify("Rust é Incrível"), "rust-e-incrivel");
//! ```
#![warn(missing_docs)]
#![warn(clippy::all)]
#![warn(clippy::pedantic)]
#![allow(clippy::must_use_candidate)]
mod error;
pub use error::ErroSlug;
/// Converte uma string em um slug URL-seguro.
///
/// Remove acentos, converte espaços em hífens, e elimina
/// caracteres não-alfanuméricos.
///
/// # Exemplos
///
/// ```rust
/// use slug_rs::slugify;
///
/// assert_eq!(slugify("Hello World"), "hello-world");
/// assert_eq!(slugify(" espaços extras "), "espacos-extras");
/// assert_eq!(slugify("já-tem-hífen"), "ja-tem-hifen");
/// assert_eq!(slugify("!!!"), "");
/// ```
pub fn slugify(texto: &str) -> String {
let sem_acentos = remover_acentos(texto);
sem_acentos
.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { '-' })
.collect::<String>()
.split('-')
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join("-")
}
/// Converte com configurações personalizadas.
///
/// # Exemplos
///
/// ```rust
/// use slug_rs::{slugify_com_opcoes, OpcoesSlug};
///
/// let opcoes = OpcoesSlug {
/// separador: '_',
/// max_comprimento: Some(10),
/// };
///
/// assert_eq!(
/// slugify_com_opcoes("Hello Beautiful World", &opcoes),
/// "hello_beau"
/// );
/// ```
pub fn slugify_com_opcoes(texto: &str, opcoes: &OpcoesSlug) -> String {
let sem_acentos = remover_acentos(texto);
let separador = opcoes.separador;
let mut slug = sem_acentos
.to_lowercase()
.chars()
.map(|c| if c.is_alphanumeric() { c } else { separador })
.collect::<String>()
.split(separador)
.filter(|s| !s.is_empty())
.collect::<Vec<_>>()
.join(&separador.to_string());
if let Some(max) = opcoes.max_comprimento {
slug.truncate(max);
// Remove separador final se truncado no meio de uma palavra
while slug.ends_with(separador) {
slug.pop();
}
}
slug
}
/// Opções para personalizar a geração de slugs.
#[derive(Debug, Clone)]
pub struct OpcoesSlug {
/// Caractere usado como separador entre palavras.
/// Padrão: `-`
pub separador: char,
/// Comprimento máximo do slug gerado.
/// `None` significa sem limite.
pub max_comprimento: Option<usize>,
}
impl Default for OpcoesSlug {
/// Cria opções com valores padrão.
///
/// # Exemplos
///
/// ```rust
/// use slug_rs::OpcoesSlug;
///
/// let opcoes = OpcoesSlug::default();
/// assert_eq!(opcoes.separador, '-');
/// assert_eq!(opcoes.max_comprimento, None);
/// ```
fn default() -> Self {
OpcoesSlug {
separador: '-',
max_comprimento: None,
}
}
}
fn remover_acentos(texto: &str) -> String {
texto.chars().map(|c| match c {
'á'|'à'|'â'|'ã'|'ä' => 'a',
'Á'|'À'|'Â'|'Ã'|'Ä' => 'A',
'é'|'è'|'ê'|'ë' => 'e',
'É'|'È'|'Ê'|'Ë' => 'E',
'í'|'ì'|'î'|'ï' => 'i',
'Í'|'Ì'|'Î'|'Ï' => 'I',
'ó'|'ò'|'ô'|'õ'|'ö' => 'o',
'Ó'|'Ò'|'Ô'|'Õ'|'Ö' => 'O',
'ú'|'ù'|'û'|'ü' => 'u',
'Ú'|'Ù'|'Û'|'Ü' => 'U',
'ç' => 'c',
'Ç' => 'C',
'ñ' => 'n',
'Ñ' => 'N',
outro => outro,
}).collect()
}
#[cfg(test)]
mod testes {
use super::*;
#[test]
fn basico() {
assert_eq!(slugify("Hello World"), "hello-world");
}
#[test]
fn acentos_portugueses() {
assert_eq!(slugify("Ação"), "acao");
assert_eq!(slugify("São Paulo"), "sao-paulo");
assert_eq!(slugify("Café"), "cafe");
}
#[test]
fn caracteres_especiais() {
assert_eq!(slugify("foo@bar.com"), "foo-bar-com");
assert_eq!(slugify("preço: R$10,50"), "preco-r-10-50");
}
#[test]
fn espacos_multiplos() {
assert_eq!(slugify(" a b c "), "a-b-c");
}
#[test]
fn string_vazia() {
assert_eq!(slugify(""), "");
assert_eq!(slugify(" "), "");
assert_eq!(slugify("!!!"), "");
}
#[test]
fn max_comprimento() {
let opcoes = OpcoesSlug {
separador: '-',
max_comprimento: Some(5),
};
assert_eq!(slugify_com_opcoes("hello world", &opcoes), "hello");
}
#[test]
fn separador_customizado() {
let opcoes = OpcoesSlug {
separador: '_',
max_comprimento: None,
};
assert_eq!(slugify_com_opcoes("Hello World", &opcoes), "hello_world");
}
}
tests/integracao.rs:
// Testes de integração — testam a API pública como um usuário externo
use slug_rs::{slugify, slugify_com_opcoes, OpcoesSlug};
#[test]
fn casos_de_uso_reais() {
// URLs de blog
assert_eq!(
slugify("Como aprender Rust em 1 ano"),
"como-aprender-rust-em-1-ano"
);
// Nomes de arquivo
assert_eq!(
slugify("Relatório Q1 2024 — Versão Final"),
"relatorio-q1-2024-versao-final"
);
// Títulos com emoji (removidos)
assert_eq!(
slugify("Rust 🦀 é incrível"),
"rust-e-incrivel"
);
}
#[test]
fn compatibilidade_urls() {
let slug = slugify("Programação em Rust: Guia Completo");
// Deve conter apenas caracteres seguros para URLs
assert!(slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'));
// Não deve começar ou terminar com hífen
assert!(!slug.starts_with('-'));
assert!(!slug.ends_with('-'));
// Não deve ter hífens consecutivos
assert!(!slug.contains("--"));
}
#[test]
fn opcoes_padrao_equivalente() {
let texto = "Hello World";
let opcoes = OpcoesSlug::default();
assert_eq!(slugify(texto), slugify_com_opcoes(texto, &opcoes));
}
examples/uso_basico.rs:
//! Exemplo básico de uso da crate slug-rs.
//!
//! Execute com: cargo run --example uso_basico
use slug_rs::{slugify, slugify_com_opcoes, OpcoesSlug};
fn main() {
println!("═══ slug-rs: Exemplos ═══
");
let exemplos = [
"Hello, World!",
"Programação em Rust",
"São Paulo — Brasil",
"preço: R$ 49,90",
"2024: O Ano do Rust 🦀",
" espaços extras ",
];
println!("── Slugify padrão ──");
for texto in &exemplos {
println!(" {:35} → {}", texto, slugify(texto));
}
println!("
── Com underscore e limite de 20 ──");
let opcoes = OpcoesSlug {
separador: '_',
max_comprimento: Some(20),
};
for texto in &exemplos {
println!(" {:35} → {}", texto, slugify_com_opcoes(texto, &opcoes));
}
}
Mantendo uma crate popular
À medida que sua crate ganha usuários, a manutenção torna-se um compromisso:
Responda issues rapidamente — mesmo que seja só para confirmar que você viu. Silêncio por semanas é desanimador para quem contribui.
Use labels no GitHub — good-first-issue atrai contribuidores novos, help-wanted para issues que você não tem tempo, breaking-change para sinalizar PRs que requerem nova versão major.
Automatize o que puder — dependabot para atualização de dependências, cargo-release para automatizar o processo de release (bump de versão, tag, publicação).
Documente decisões — um arquivo DESIGN.md ou ADRs (Architecture Decision Records) explicando por que certas escolhas foram feitas economiza horas de discussão em issues futuras.
Fontes e leituras recomendadas
- "The Cargo Book" — referência completa — https://doc.rust-lang.org/cargo/
- "Semantic Versioning 2.0.0" — especificação semver — https://semver.org/lang/pt-BR/
- "SemVer Compatibility" — The Cargo Book — o que constitui breaking change — https://doc.rust-lang.org/cargo/reference/semver.html
cargo-release— automação de releases — https://github.com/crate-ci/cargo-releasecargo-audit— auditoria de dependências — https://github.com/rustsec/rustsec- "API Guidelines" — convenções para crates públicas — https://rust-lang.github.io/api-guidelines/
- crates.io — repositório central — https://crates.io
- lib.rs — busca alternativa com melhor UX — https://lib.rs
Artigo #39 de 52 | Série: Dominando Rust em 1 Ano Próximo → Artigo #40: Rust para CLI — Construindo ferramentas de linha de comando profissionais com clap