O Cargo é muito mais do que um simples gerenciador de dependências: é o coração do ecossistema Rust, responsável por build, teste, documentação e publicação. Nesta aula, vamos nos aprofundar em recursos avançados que permitem controlar com precisão como seu projeto é compilado, quais dependências são incluídas em cada contexto e como automatizar tarefas antes e depois da compilação. Você sairá daqui capaz de configurar projetos complexos com múltiplas features, otimizações agressivas para produção e scripts de build personalizados.

Dominar esses recursos é essencial para qualquer projeto Rust sério, seja uma biblioteca open-source, um serviço web ou um binário de linha de comando. Vamos começar desvendando o sistema de features, que permite ligar e desligar funcionalidades do seu código de forma declarativa.

Features

Features são um mecanismo poderoso do Cargo para compilação condicional. Elas permitem que você inclua ou exclua partes do código, dependências ou configurações com base em flags ativadas no momento da compilação. Isso é extremamente útil para oferecer diferentes níveis de funcionalidade, como uma versão leve e uma versão completa da sua biblioteca, ou para integrar-se opcionalmente a outras bibliotecas.

No arquivo Cargo.toml, você define features na seção [features]. Cada feature pode ativar outras features ou dependências opcionais. O Cargo também possui uma feature padrão chamada default, que é ativada por padrão, mas pode ser desativada pelo consumidor do seu crate.

Vamos ver um exemplo prático. Suponha que você esteja criando uma biblioteca de processamento de imagens e queira oferecer suporte opcional ao formato PNG. Você pode definir uma feature png que adiciona a dependência png apenas quando ativada.

[features]
default = ["std"]
std = []
png = ["dep:png"]

[dependencies]
png = { version = "0.17", optional = true }

[features]
# ...

No código, você usa a macro #[cfg(feature = "png")] para incluir o código somente quando a feature estiver ativa. Isso permite que o consumidor escolha se quer suporte a PNG ou não, sem pagar o custo de compilação e de dependências desnecessárias.

#[cfg(feature = "png")]
pub fn load_png(path: &str) -> Image {
    // implementação específica para PNG
}

#[cfg(not(feature = "png"))]
pub fn load_png(path: &str) -> Image {
    panic!("PNG support not enabled");
}

As features também podem ser usadas para ativar outras features. Por exemplo, uma feature full que ativa todas as outras:

[features]
default = ["std"]
std = []
png = []
jpeg = []
full = ["png", "jpeg"]

Isso é útil para oferecer uma opção única que liga tudo.

Uma boa prática é sempre documentar suas features no README e usar nomes claros. Além disso, evite features que não tenham um propósito claro, pois elas aumentam a complexidade do projeto.

Profiles (dev/release)

Os profiles no Cargo definem configurações de compilação diferentes para diferentes cenários. Por padrão, existem dois perfis: dev (usado com cargo build e cargo run) e release (usado com cargo build --release). O perfil dev é otimizado para velocidade de compilação e depuração, enquanto o release é otimizado para desempenho do código gerado.

Você pode personalizar esses perfis na seção [profile.dev] e [profile.release] no Cargo.toml. As opções mais comuns incluem:

  • opt-level — nível de otimização (0-3, ou "s" para tamanho).
  • debug — gera informações de depuração (true/false, ou nível).
  • lto — Link-Time Optimization (false, true, "thin").
  • codegen-units — número de unidades de geração de código.
  • panic — estratégia de pânico ("unwind" ou "abort").

Exemplo de configuração para um perfil de release agressivo:

[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"

Isso produz um binário menor e mais rápido, mas aumenta o tempo de compilação.

Você também pode criar perfis personalizados, como bench ou dist, e depois usá-los com cargo build --profile dist. Isso é útil para cenários específicos, como compilações para produção com características diferentes das padrão.

Entender os profiles é crucial para garantir que seu código seja executado de forma eficiente em produção, mantendo a experiência de desenvolvimento agradável.

Dependências de dev

Dependências de desenvolvimento (dev-dependencies) são aquelas usadas apenas em testes, exemplos, benchmarks ou ferramentas internas. Elas não são incluídas quando o crate é usado como dependência de outro projeto. Isso mantém o tamanho e o tempo de compilação do consumidor reduzidos.

No Cargo.toml, você define as dependências de dev na seção [dev-dependencies]. Por exemplo, ao criar uma biblioteca, você pode querer usar o crate pretty_assertions para melhorar as mensagens de teste, mas não quer que seus usuários tenham que compilar isso.

[dev-dependencies]
pretty_assertions = "1.4"

Essas dependências estão disponíveis para testes (cargo test), exemplos (cargo run --example) e benchmarks (cargo bench). Elas também podem ser usadas em código dentro de tests/, examples/ e benches/.

Um caso comum é usar um crate como criterion para benchmarks. Ele seria uma dev-dependency, pois só é necessário quando você está desenvolvendo o crate, não quando o usa.

É importante não colocar dependências que são necessárias em tempo de execução como dev-dependencies, pois elas não estarão disponíveis para os consumidores do seu crate.

Outra prática é usar dev-dependencies para ferramentas de geração de código ou testes de integração, mantendo o escopo do crate limpo.

Scripts de build

Scripts de build são arquivos (geralmente build.rs) que são compilados e executados antes da compilação do seu crate. Eles são usados para tarefas como gerar código, compilar bibliotecas C, configurar o ambiente ou verificar condições do sistema.

O script de build é declarado no Cargo.toml com a chave build:

build = "build.rs"

Dentro do build.rs, você pode usar a biblioteca std::env para acessar variáveis de ambiente fornecidas pelo Cargo, como CARGO_MANIFEST_DIR (diretório do seu projeto) ou OUT_DIR (diretório de saída para artefatos gerados). O script pode gerar arquivos em OUT_DIR e incluí-los no seu crate com a macro include!.

Exemplo de um script de build simples que gera um arquivo de versão:

use std::env;
use std::fs;
use std::path::Path;

fn main() {
    let out_dir = env::var("OUT_DIR").unwrap();
    let dest_path = Path::new(&out_dir).join("version.rs");
    let version = env::var("CARGO_PKG_VERSION").unwrap();
    fs::write(dest_path, format!("pub const VERSION: &str = \"{}\";\n", version)).unwrap();
    println!("cargo:rerun-if-changed=build.rs");
}

No seu código, você pode incluir o arquivo gerado:

include!(concat!(env!("OUT_DIR"), "/version.rs"));

fn main() {
    println!("Versão: {}", VERSION);
}

Scripts de build também podem emitir instruções para o Cargo, como cargo:rerun-if-changed para indicar quando o script deve ser reexecutado, ou cargo:rustc-link-lib para linkar bibliotecas nativas.

É importante que o script de build seja determinístico e não dependa de estado externo não rastreado, para evitar builds inconsistentes.

Boas práticas e observações finais

Ao trabalhar com features, profiles, dev-dependencies e scripts de build, algumas boas práticas ajudam a manter o projeto saudável:

  • Documente todas as features e o que elas ativam.
  • Use features para evitar dependências pesadas em quem usa seu crate.
  • Perfis de release devem ser otimizados, mas lembre-se de que otimizações extremas aumentam o tempo de compilação.
  • Mantenha dev-dependencies separadas das dependências normais para não inflar o crate final.
  • Scripts de build devem ser rápidos e não fazer nada surpreendente; use cargo:rerun-if-changed para evitar reexecuções desnecessárias.

Com esses recursos, você pode criar projetos Rust profissionais e flexíveis, adaptáveis a diferentes necessidades.

Referências

Exercícios

  1. Crie um projeto Cargo chamado meu_crate com uma feature extra que, quando ativada, imprime "Modo extra" ao executar um binário. Desative a feature padrão e mostre como ativar a feature na linha de comando.
  2. ✓ Resposta: No Cargo.toml:
    [package]
    name = "meu_crate"
    version = "0.1.0"
    edition = "2021"
    
    [features]
    default = []
    extra = []
    
    No main.rs:
    fn main() {
        #[cfg(feature = "extra")]
        println!("Modo extra");
    }
    
    Para compilar com a feature: cargo run --features extra.
  3. Configure o perfil release do seu projeto para usar nível de otimização 3, LTO verdadeiro e apenas 1 unidade de codegen. Explique o impacto esperado.
  4. ✓ Resposta: No Cargo.toml:
    [profile.release]
    opt-level = 3
    lto = true
    codegen-units = 1
    
    Isso aumentará o tempo de compilação, mas produzirá um binário mais rápido e menor.
  5. Adicione uma dev-dependency rand ao seu projeto e escreva um teste que usa rand para gerar um número aleatório e verificar se ele está dentro de um intervalo.
  6. ✓ Resposta: No Cargo.toml:
    [dev-dependencies]
    rand = "0.8"
    
    No arquivo de teste (ex.: tests/random_test.rs):
    use rand::Rng;
    
    #[test]
    fn test_random_in_range() {
        let mut rng = rand::thread_rng();
        let n: u32 = rng.gen_range(1..=10);
        assert!(n >= 1 && n <= 10);
    }
    
  7. Crie um script de build que defina uma variável de ambiente MINHA_VAR com o valor 42 e a use no código principal para imprimir esse valor.
  8. ✓ Resposta: No Cargo.toml:
    build = "build.rs"
    
    No build.rs:
    fn main() {
        println!("cargo:rustc-env=MINHA_VAR=42");
    }
    
    No código principal:
    fn main() {
        println!("MINHA_VAR = {}", env!("MINHA_VAR"));
    }
    
  9. Explique a diferença entre dependências normais e dev-dependencies e por que é importante separá-las.
  10. ✓ Resposta: Dependências normais são necessárias para o funcionamento do crate em tempo de execução e são incluídas quando o crate é usado como dependência. Dev-dependencies são usadas apenas durante o desenvolvimento (testes, exemplos, benchmarks) e não são propagadas para os consumidores. Separar evita que dependências desnecessárias aumentem o tamanho e o tempo de compilação dos projetos que dependem do seu crate.