createrepo_rs
Pure-Rust drop-in for createrepo_c. Zero FFI. Single static binary. Parallel by default.
$ time createrepo_rs /srv/zabbix/ --dump-manifest # 254 RPMs, 80 cores
real 0m0.078s β was 10.5s with rpm -K loop (130Γ)
$ time createrepo_rs /srv/zabbix/ # full repodata gen
real 0m1.87s β createrepo_c: 2.15s, capped at 5 workers
Production-tested: Zabbix 7.2 mirror, Debian 13, 80-core. Ships in COPR / AUR / Debian.
Why
I needed fast RPM metadata generation for a private Zabbix mirror.
createrepo_c works, but it pulls librpm, libxml2, glib2, zchunk β packaging pain on Debian.
And rpm -K loops for manifest scanning were taking 10+ seconds in CI.
Rewrote it in Rust: parallel worker pool, in-memory SQLite (single VACUUM INTO flush), zero shared libs.
The result ships as a 3.5 MB musl static binary with no runtime dependencies.
vs createrepo_c
| createrepo_c | createrepo_rs | |
|---|---|---|
| Dependencies | librpm, libxml2, glib2, zchunk... | zero FFI β pure Rust crates |
| Binary | ~200KB + shared libs | 3.5MB musl static |
| Max workers | 5 (hardcoded) | all CPUs (auto) |
| Memory safety | manual malloc/free | borrow checker |
| Cross-compile | painful | cargo zigbuild |
--dump-manifest |
β need rpm -K loop |
β parallel, 0.078s/254 pkgs |
| Signature detection | β separate rpm -K |
β built-in |
| I/O timeout | β | β
--timeout watchdog |
| dnf compatible | β | β verified |
| CLI params | 55 | 53 |
Install
# crates.io
# Fedora / RHEL / CentOS (COPR)
&&
# Arch Linux (AUR)
# Debian / Ubuntu
# RHEL / CentOS
# Docker
# or: ghcr.io/jamesarch/createrepo-rs /data
# Basic usage
# Production example
CI/CD
# GitHub Actions
- uses: docker://ghcr.io/jamesarch/createrepo-rs:0.1.9
with:
args: ./rpms --baseurl=https://repo.example.com --compress-type=zstd
# GitLab CI
generate-repodata:
image: ghcr.io/jamesarch/createrepo-rs:0.1.9
script:
- createrepo_rs ./rpms --compress-type=zstd
Container tags: latest Β· 0.1.9 Β· 0.1 Β· sha-<short-sha>
Performance
Benchmarks on Zabbix production server (Debian 13, 80-core, 254 RPMs):
Full repodata generation (zstd)
| Tool | Time | CPU |
|---|---|---|
| createrepo_c | 2.15s | 454% (capped at 5 workers) |
| createrepo_rs | 1.87s | 1724% (all 80 cores) |
--dump-manifest β parallel package inventory scan
Reads only RPM signature header + name/version/arch. Skips files, deps, changelogs.
Replaces rpm -K + rpm -qp loops in CI pipelines.
| Method | Time | Workers |
|---|---|---|
rpm -K loop (bash) |
10.5s | 1 |
--dump-manifest |
0.078s | 80 (auto) |
Output: JSON lines, one object per package, with signature detection.
}
}
Worker scaling (full gen, 80-core)
| Workers | Time |
|---|---|
| 1 | 10.5s |
| 4 | 3.6s |
| 80 (auto) | 1.87s |
Architecture
In-memory SQLite
SQLite builds in RAM, flushes once at finish:
parse RPMs (parallel) βββΊ insert_package() βββΊ RAM (single connection)
β
VACUUM INTO repomd.sqlite
Traditional: one transaction + fsync per package. This: all inserts in RAM, one disk write.
I/O resilience
Network mounts (NFS, CIFS, FUSE) can stall indefinitely. Mitigations:
--timeout=Nwatchdog thread β forced exit on hangrecv_timeout(300s)on result collectionsend_timeout(30s)on job submissioncatch_unwindper worker β one bad RPM can't crash the pool
Source layout
createrepo_rs/
βββ build.rs # git hash + timestamp baked into --version
βββ lib.rs # library root + prelude re-exports
βββ src/main.rs # CLI orchestration, --dump-manifest
βββ cli/mod.rs # clap parser (53 params)
βββ pool/mod.rs # parallel worker pool (crossbeam + catch_unwind)
βββ rpm/mod.rs # RPM header parsing via `rpm` crate
βββ types/mod.rs # Package, Dependency, RepomdRecord
βββ compression/ # gzip, bzip2, zstd, xz
βββ db/mod.rs # in-memory SQLite + VACUUM INTO
βββ xml/ # repomd.xml generation + parse (for --update)
βββ walk/mod.rs # directory traversal with glob exclude
Features
Core
- primary.xml, filelists.xml, other.xml, repomd.xml β dnf-compatible
- In-memory SQLite β
VACUUM INTOflush at finish - Multi-threaded RPM parsing (auto-detects CPU count)
--timeout=Nwatchdog for stuck I/O--dump-manifestβ parallel JSON-lines inventory with signature detection--updateincremental mode withArc<Package>cache- Graceful Ctrl+C,
catch_unwindpanic recovery
Dependency extraction
- Provides / Requires / Conflicts / Obsoletes / Suggests / Enhances / Recommends / Supplements
- Full EVR (Epoch:Version-Release) parsing + flags (EQ, LT, GT, LE, GE)
Compression: gzip (default) Β· zstd Β· xz Β· bzip2
Key CLI flags
| Flag | Description |
|---|---|
--workers=N |
Parallel threads (default: all CPUs) |
--timeout=N |
Watchdog timeout in seconds |
--dump-manifest |
JSON-lines inventory + signature detection |
--compress-type=zstd |
Compression algorithm |
--no-database |
Skip SQLite generation |
--update |
Incremental mode |
--retain-old-md-by-age=30d |
Auto-cleanup old metadata |
--compatibility |
Max compat mode (gzip + simple filenames) |
Full reference: createrepo_rs --help (53 params)
Library usage
[]
= "0.1"
use *;
// Parse an RPM
let mut reader = open.unwrap;
let pkg = reader.read_package.unwrap;
println!;
// Lightweight manifest scan (header only)
let entry = reader.read_manifest_entry.unwrap;
println!;
Build
# musl static binary (requires cargo-zigbuild + zig)
# ARM64
Docker test
&&
# verifies dnf can consume the generated repodata
Changelog
v0.1.8 β parallel --dump-manifest Β· read_manifest_entry() header-only Β· 254 pkgs: 10.5s β 0.078s
v0.1.7 β --dump-manifest Β· is_signed() PGP/RSA/DSA detection Β· ManifestEntry
v0.1.6 β in-memory SQLite Β· single VACUUM INTO flush Β· removed per-struct boilerplate
v0.1.5 β --timeout watchdog Β· recv_timeout Β· catch_unwind Β· build info in --version
v0.1.4 β initial public release Β· 52 CLI params Β· dnf-compatible output
License
GPL-2.0-or-later β same as createrepo_c.
Built with rpm-rs/rpm Β· quick-xml Β· rusqlite Β· crossbeam
EVR parsing reference: dralley/rpmrepo_metadata (@dralley, Red Hat)
Original C implementation: rpm-software-management/createrepo_c