Charmlint Rust backend

A compiled alternative to the Python charmlint that completes a full lint run in under 30 ms.

Background

Charmlint is Cantrip's deterministic charm linter. It checks charms against 40+ rules across 12 categories — metadata completeness, COS integration, testing, deprecated APIs, actions, config quality, security, structure, documentation, libraries, charmcraft compatibility, and status reporting.

Unlike quickpack, charmlint is a pure CPU tool with no subprocess calls. All the work is YAML parsing, regex matching, and file traversal — exactly the kind of workload where Rust excels. The Rust implementation (src/charmlint-rs/) delivers the full speedup with no bottleneck to cap it.

Architecture

The Rust binary mirrors the Python package:

Python module Rust module Responsibility
cli.py main.rs CLI argument parsing (clap), output formatting
linter.py linter.rs Rule filtering, severity overrides, min-severity
rules/*.py rules.rs All 40+ rules (single file, grouped by category)
models.py models.rs Diagnostic, Severity, CharmContext, LintReport
config.py config.rs .charmlint.yaml loader
context.rs Charm context builder (YAML, Python sources, tests)

Both implementations share the same command-line interface:

charmlint [path] [--format text|json] [--select CATS] [--ignore RULES]
         [--severity LEVEL] [--config PATH] [--strict] [--no-colour]

The Rust version produces identical JSON output structure, identical rule IDs and messages, and identical exit codes (0 = clean, 1 = errors, 2 = warnings in strict mode).

How the backend is selected

When the agent's charmlint tool is invoked, it checks for the Rust binary in two locations:

  1. charmlint-rs on $PATH.
  2. The in-tree build artefact at src/charmlint-rs/target/release/charmlint.

If found, the tool invokes the binary with --format json, parses the output, and returns it to the agent. If neither location has a binary, it falls back to the Python charmlint library transparently. The backend field in the tool result data indicates which implementation was used.

The charm_audit tool always uses the Python library directly because it needs to inspect individual Diagnostic objects for structured report generation.

Performance

Benchmarks on an Ubuntu 24.04 machine (x86_64), linting a minimal charm project:

Metric Rust Python Speedup
Startup (--help) ~33 ms ~177 ms 5.4×
Full lint (all rules) ~27 ms ~181 ms 6.7×

The full lint run in Rust is actually faster than Python’s startup alone. Since charmlint is a pure CPU tool — YAML parsing, regex matching, and filesystem stat calls — there is no subprocess bottleneck to limit the speedup.

In the agent's build–test–debug loop, charmlint typically runs after every code change. At 27 ms, it adds negligible latency to the iteration cycle.

Building from source

The Rust toolchain (1.70+) is required. From the repository root:

cd src/charmlint-rs
cargo build --release

The binary is written to src/charmlint-rs/target/release/charmlint. Cantrip will find it automatically at this location. To install system-wide:

sudo cp src/charmlint-rs/target/release/charmlint /usr/local/bin/charmlint-rs

Running the tests

The Rust implementation has its own spread test suite at tests/spread/charmlint-rs/task.yaml. It runs 43 of the 45 tests from the Python suite (excluding the python -m charmlint test). To run locally:

spread tests/spread/charmlint-rs

Limitations

See also: