O Cargo é o coração do ecossistema Rust, indo muito além de um simples gerenciador de dependências. Ele oferece um sistema de build robusto e altamente configurável, permitindo que você controle desde a compilação de código até a publicação de crates. Nesta aula, vamos nos aprofundar em quatro recursos avançados que todo desenvolvedor Rust profissional deve dominar: features, perfis (profiles), dependências de desenvolvimento (dev-dependencies) e scripts de build. Com esses recursos, você poderá criar projetos flexíveis, otimizados e adaptados a diferentes cenários, como desenvolvimento, testes e produção.

Esses recursos não apenas melhoram a produtividade, mas também garantem que seu código seja compilado de forma eficiente e com as otimizações adequadas. Vamos explorar cada um deles com exemplos práticos, mostrando como aplicá-los em projetos reais.

Features

Features são um mecanismo poderoso do Cargo para ativar ou desativar funcionalidades opcionais em um crate. Elas permitem que você compile apenas o que é necessário, reduzindo o tempo de compilação e o tamanho do binário. Features são declaradas no arquivo Cargo.toml e podem ser usadas para controlar dependências opcionais, incluir ou excluir código e até mesmo definir comportamentos específicos.

Uma feature é basicamente um rótulo que agrupa um conjunto de dependências ou flags de compilação. Quando você ativa uma feature, o Cargo inclui as dependências associadas e define as variáveis de ambiente correspondentes. Por exemplo, imagine um crate que suporta tanto JSON quanto YAML para serialização. Em vez de sempre incluir ambas as bibliotecas, você pode definir features para que o usuário escolha qual suporte deseja.

# Cargo.toml
[features]
default = ["json"]
json = ["dep:serde_json"]
yaml = ["dep:serde_yaml"]

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }

No exemplo acima, definimos duas features: json e yaml. A feature default inclui json por padrão. As dependências serde_json e serde_yaml são opcionais e só serão compiladas se a feature correspondente for ativada. Para usar YAML, o usuário pode executar cargo build --features yaml ou adicionar a feature ao seu próprio crate.

Além de controlar dependências, features podem ser usadas para incluir ou excluir código no seu próprio crate. Você pode usar a macro cfg! ou atributos como #[cfg(feature = "...")] para condicionar a compilação de módulos ou funções. Isso é útil para fornecer funcionalidades extras que não devem estar presentes em todas as builds.

// src/lib.rs
#[cfg(feature = "extra")]
pub fn funcao_extra() {
    println!("Recurso extra ativado!");
}

pub fn funcao_basica() {
    println!("Funcionalidade básica.");
}

Para ativar a feature extra, basta adicioná-la ao Cargo.toml e compilar com --features extra. As features também podem ser combinadas e dependências entre si, permitindo construir um sistema complexo de opções.

Profiles (dev/release)

Profiles são configurações de compilação que definem como o código é otimizado e compilado. O Cargo oferece dois perfis principais: dev (padrão durante o desenvolvimento) e release (para produção). Cada perfil pode ser personalizado no Cargo.toml para ajustar níveis de otimização, debug info, e até mesmo o nível de warnings.

O perfil dev é otimizado para velocidade de compilação e melhor experiência de debug, enquanto o perfil release é otimizado para desempenho do executável final. Por padrão, o perfil dev não aplica otimizações (para compilar mais rápido) e inclui informações de debug. Já o perfil release aplica otimizações agressivas (opt-level = 3) e omite informações de debug.

Você pode customizar esses perfis ou criar novos perfis personalizados. Por exemplo, pode querer um perfil intermediário para testes de performance ou um perfil com otimizações para tamanho do binário. A configuração é feita na seção [profile.*] do Cargo.toml.

# Cargo.toml
[profile.dev]
opt-level = 1  # Otimiza um pouco, mas mantém debug rápido

[profile.release]
opt-level = 3  # Otimização máxima
lto = true     # Link-time optimization
codegen-units = 1 # Melhora otimizações, mas aumenta tempo de compilação

No exemplo, ajustamos o perfil dev para ter um nível de otimização 1, útil quando você quer testar otimizações sem perder muito tempo de compilação. No perfil release, ativamos LTO (Link-Time Optimization) e reduzimos as unidades de geração de código para melhorar o desempenho do binário final, à custa de tempo de compilação.

Além dos perfis padrão, você pode criar perfis customizados com [profile.custom] e usá-los com cargo build --profile custom. Isso é útil para cenários específicos, como builds para análise estática ou para distribuição.

Dependências de dev

Dependências de desenvolvimento (dev-dependencies) são bibliotecas que são usadas apenas em testes, exemplos ou benchmarks, e não fazem parte do código de produção. Elas são declaradas na seção [dev-dependencies] do Cargo.toml. Isso mantém o escopo das dependências de produção limpo e reduz o tamanho do binário final.

As dev-dependencies são especialmente úteis para incluir ferramentas de teste, como proptest, criterion (para benchmarks) ou crates de mocking. Elas são compiladas apenas quando você executa cargo test, cargo bench ou cargo build --examples.

# Cargo.toml
[dev-dependencies]
proptest = "1.4"
criterion = "0.5"

No exemplo, adicionamos proptest para testes baseados em propriedades e criterion para benchmarks. Essas crates não serão incluídas quando você compilar o crate para produção com cargo build --release.

É importante notar que as dev-dependencies também podem ter features, assim como as dependências normais. Você pode ativar features específicas apenas para testes, usando a mesma sintaxe de features. Isso permite que você teste funcionalidades experimentais sem afetar o código de produção.

Scripts de build

Scripts de build (build scripts) são arquivos executáveis que o Cargo compila e executa antes de compilar o crate. Eles são escritos em Rust e declarados no campo build do Cargo.toml. O script pode gerar código, configurar o ambiente de compilação, verificar dependências do sistema, entre outras tarefas.

Os scripts de build são especialmente úteis para integrar bibliotecas C/C++ ou gerar código a partir de arquivos de configuração, como bindings de FFI. O script pode emitir instruções especiais via cargo:rerun-if-changed para otimizar a rebuild, e pode definir variáveis de ambiente que ficam disponíveis para o código do crate via a macro env!.

// build.rs
fn main() {
    println!("cargo:rerun-if-changed=src/config.txt");
    let config = std::fs::read_to_string("src/config.txt").unwrap();
    println!("cargo:rustc-env=CONFIG={}", config);
    // Gera um arquivo de código
    std::fs::write("src/generated.rs", "pub fn gerada() {}").unwrap();
}

No exemplo, o script lê um arquivo de configuração e define uma variável de ambiente CONFIG que pode ser usada no código com env!("CONFIG"). Também gera um arquivo generated.rs que pode ser incluído no crate com include!.

Para ativar um script de build, adicione build = "build.rs" ao Cargo.toml. O script é compilado e executado automaticamente pelo Cargo antes de compilar o crate. É importante que o script seja rápido e não dependa de recursos não determinísticos, pois isso pode afetar a reprodutibilidade das builds.

Boas práticas

Ao usar esses recursos avançados, é importante seguir algumas boas práticas para manter seu projeto limpo e eficiente. Primeiro, documente bem suas features e perfis, explicando quando usar cada um. Isso ajuda outros desenvolvedores a entender as opções disponíveis. Em segundo lugar, mantenha as dev-dependencies separadas das dependências de produção, para não inflar o binário final. Terceiro, escreva scripts de build idempotentes e rápidos, evitando gerar código desnecessariamente. Por fim, use perfis para otimizar builds de acordo com o contexto, mas não exagere nas otimizações, pois podem aumentar o tempo de compilação.

Exercícios

  1. Explique a diferença entre dependencies e dev-dependencies no Cargo. Dê um exemplo de uso para cada tipo.

    ✓ Resposta: dependencies são usadas no código de produção, enquanto dev-dependencies são usadas apenas em testes, exemplos e benchmarks. Exemplo: serde como dependency e proptest como dev-dependency.
  2. Como você ativaria uma feature chamada serde em um crate chamado meu-crate usando a linha de comando?

    ✓ Resposta: Execute cargo build --features serde no diretório do crate.
  3. Qual é a diferença entre os perfis dev e release em termos de otimização e tempo de compilação?

    ✓ Resposta: O perfil dev tem otimização leve (padrão 0) e compila mais rápido, enquanto release tem otimização alta (padrão 3) e compila mais devagar.
  4. Escreva um script de build simples que imprima uma mensagem no terminal e defina uma variável de ambiente MINHA_VAR com o valor 42.

    ✓ Resposta:
    fn main() {
        println!("Executando build script");
        println!("cargo:rustc-env=MINHA_VAR=42");
    }
  5. Como você criaria um perfil personalizado chamado perf com otimização nível 2 e sem debug info, e como o usaria?

    ✓ Resposta: Adicione no Cargo.toml:
    [profile.perf]
    opt-level = 2
    debug = false
    e use com cargo build --profile perf.

Referências