A Rust ökoszisztéma egyik legszebb tulajdonsága, hogy a Cargo rendkívül könnyen bővíthető: bármilyen cargo-xyz nevű binárist telepítesz, azt a cargo xyz paranccsal azonnal alparancsként tudod hívni. Ez a rugalmasság hívta életre azt a kis eszköztárat, amit ma szinte minden komolyabb Rust projektben megtalálsz. Nézzük sorra, mik ezek, és hogyan illesztjük össze őket egy működő CI-pipeline-ba.

Miért van szükség egyáltalán bővítményekre?

A beépített cargo test, cargo build és cargo update parancsok jól működnek, de van pár terület, ahol a közösség egyszerűen jobbat épített:

  • a tesztfuttatás sebessége és megbízhatósága,
  • a függőségi lánc biztonsági auditálása,
  • az elavult crate-ek felderítése és frissítése.
Megjegyzés

Egyik említett eszköz sem cseréli le a Cargo alapfunkcióit — mind ráépülnek, és a Cargo.toml/Cargo.lock fájlokat használják forrásként.

cargo-nextest: villámgyors, megbízható tesztfuttatás

A cargo-nextest egy teljesen új tesztfuttató motor, ami a cargo test helyett fut le, de kompatibilis a standard #[test] attribútummal. A legnagyobb előnye, hogy minden tesztet külön folyamatban (process) indít el, így egy pánikoló vagy végtelen ciklusba ragadt teszt nem viszi magával az egész futtatást.

Telepítés:

cargo install cargo-nextest --locked

Használat gyakorlatilag ugyanaz, mint a beépített tesztfuttatónál:

cargo nextest run

Nézzünk egy egyszerű tesztelhető modult, amit a cargo nextest run ugyanúgy megtalál, mint a cargo test:

// src/lib.rs
pub fn parse_config_line(line: &str) -> Option<(String, String)> {
    let Some((key, value)) = line.split_once('=') else {
        return None;
    };

    Some((key.trim().to_string(), value.trim().to_string()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn parses_valid_line() {
        let result = parse_config_line("name = rustacean");
        assert_eq!(
            result,
            Some(("name".to_string(), "rustacean".to_string()))
        );
    }

    #[test]
    fn rejects_invalid_line() {
        assert_eq!(parse_config_line("no-equal-sign"), None);
    }
}

A fenti kódban egy let-else konstrukciót használtunk, ami a Rust 1.65 óta stabil, és kifejezetten jól illik az "korai visszatérés hiba esetén" mintára.

A cargo-nextest emellett szép, színes, összegzett kimenetet ad, és sokkal jobban skálázódik sok mag esetén, mint a beépített futtató. Nagyobb projekteknél (több száz teszt) simán 2-3x gyorsulást tapasztalhatsz.

Tipp

A cargo nextest run --no-capture paranccsal élőben láthatod a println! kimeneteket is, ami a beépített cargo test -- --nocapture-nél kényelmesebb szintaxis.

cargo-audit: biztonsági rés keresés a függőségekben

A cargo-audit a RustSec adatbázis alapján ellenőrzi a Cargo.lock fájlban szereplő összes crate-et, és figyelmeztet, ha valamelyikben ismert biztonsági sebezhetőséget találtak.

cargo install cargo-audit --locked
cargo audit

Ha a futtatás talál valamit, a kimenet pontosan megmutatja, melyik crate melyik verziójában van a probléma, és milyen verzióra érdemes frissíteni.

Figyelem

A cargo audit csak az ismert és publikált sebezhetőségeket találja meg. Nem helyettesíti a kódreview-t vagy a statikus elemzést, csak egy fontos, olcsó biztonsági háló.

Érdemes a cargo audit-ot a CI-ba beépíteni, hogy minden pull requestnél lefusson — így nem csúszik be észrevétlenül egy sérülékeny tranzitív függőség.

cargo-outdated és cargo-edit: a függőségek karbantartása

A cargo-outdated megmutatja, mely függőségeidnek van újabb verziója, méghozzá külön jelezve, hogy az adott frissítés kompatibilis-e a semver szabályok szerint, vagy major verzióváltást igényel:

cargo install cargo-outdated --locked
cargo outdated

A cargo-edit pedig a Cargo.toml szerkesztését teszi kényelmesebbé parancssorból. Bár a cargo add alapfunkciója már beépült a Cargo-ba, a cargo-edit csomag még mindig hasznos kiegészítő parancsokat ad, például a cargo upgrade-et, amivel egy lépésben tudod a Cargo.toml-ban szereplő verziómegkötéseket is a legfrissebbre húzni (nem csak a lockfájlt):

cargo install cargo-edit --locked
cargo upgrade --workspace
Jó tudni

A cargo upgrade a Cargo.toml-t módosítja, tehát ha szigorú verziómegkötéseid vannak, mindig nézd át a diffet commit előtt.

Egy tipikus karbantartási rutin heti szinten:

  1. cargo outdated — áttekintés, mi van hátra
  2. cargo upgrade --workspace — kompatibilis frissítések bevezetése
  3. cargo test / cargo nextest run — minden még működik-e
  4. cargo audit — nincs-e új sebezhetőség

A sparse registry protokoll: a jövő (majdnem itt van)

Ha valaha vártál percekig, amíg a Cargo letölti a teljes crates.io indexet egy tiszta cargo build során, akkor tudod, miért fontos hír ez. A Cargo csapata jelenleg dolgozik egy úgynevezett sparse registry (ritkás index) protokollon, ami a jelenlegi teljes git-alapú index klónozása helyett HTTP-n keresztül, igény szerint tölti le csak azoknak a crate-eknek a metaadatait, amikre ténylegesen szükség van.

Jelenleg (2023 januárjában) ez még kísérleti, nightly Rust alatt, feature flag mögött érhető el:

CARGO_UNSTABLE_SPARSE_REGISTRY=true cargo +nightly build -Z sparse-registry
Megjegyzés

A sparse registry még nem stabil funkció — a nyilvános tracking issue és az RFC alapján a Cargo csapata a közelgő 1.68-as kiadásra tervezi a stabilizálását. Éles projektben egyelőre ne építs rá.

Amikor stabilizálódik, a nagyvállalati, sok mikroszolgáltatásos monorepókban jelentős build-idő csökkenést hozhat majd, mert a CI runnerek nem fogják minden alkalommal újraklónozni a teljes crates.io indexet.

Minden együtt: egy egyszerű CI workflow

Állítsuk össze a fenti eszközöket egy GitHub Actions workflow-ba. A cél: minden pushnál fusson le a build, a nextest, az audit és az outdated-ellenőrzés (ez utóbbi csak figyelmeztet, nem buktatja el a buildet).

name: CI

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Install stable Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo registry
        uses: actions/cache@v3
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}

      - name: Install cargo-nextest
        run: cargo install cargo-nextest --locked

      - name: Install cargo-audit
        run: cargo install cargo-audit --locked

      - name: Build
        run: cargo build --all-features

      - name: Run tests
        run: cargo nextest run

      - name: Security audit
        run: cargo audit

      - name: Check outdated dependencies
        run: cargo install cargo-outdated --locked && cargo outdated || true

Ez a pipeline négy dolgot garantál minden egyes commit után: fordul-e a projekt, mennek-e a tesztek, nincs-e ismert biztonsági rés, és van-e elmaradás a függőségek frissítésében. A cargo-outdated lépést szándékosan nem buktatóként (|| true) állítottam be, mert az elavult csomagok önmagukban nem hibák — csak infók, amikre reagálni érdemes, de nem feltétlenül azonnal.

Tipp

Ha a build ideje szűk keresztmetszet, a Cargo.lock hash-elésével cache-elt registry könyvtár rengeteget spórol — különösen igaz ez lesz majd a sparse registry stabilizálása után, amikor a metaadat-letöltés is gyorsabb lesz.

Összefoglalás

2023 elején a Rust tooling-ökoszisztéma már annyira érett, hogy a cargo test és a cargo update helyett érdemes rögtön a cargo-nextest, cargo-audit, cargo-outdated és cargo-edit négyesére állni. Ezek nem versenyeznek a beépített Cargo funkciókkal, hanem kiegészítik őket ott, ahol a közösség konkrét fájdalmas pontokra talált elegáns megoldást. A sparse registry protokoll pedig egy jó példa arra, hogy a Cargo csapata folyamatosan dolgozik az alapinfrastruktúra gyorsításán is — érdemes lesz figyelni, amikor pár hónapon belül stabilizálódik. Addig is: telepítsd fel a fenti négy eszközt, illeszd be őket a CI-be, és élvezd a gyorsabb, biztonságosabb fejlesztői kört.