rv: The Modern R Package Manager for Reproducible Workflows
Introduction
I’ve been using renv for years to manage R dependencies, and while it gets the job done, I’ve always felt something was missing. The iterative workflow, the occasional dependency conflicts, the slow installations… it works, but it doesn’t feel modern.
Then I discovered rv — a declarative R package manager written in Rust — and it immediately clicked. If you’ve been following the Python ecosystem like I have, you’ve probably seen uv revolutionize Python package management. Well, rv brings that same philosophy to R: declarative configuration, fast execution, and true reproducibility. This is exactly what I’ve been waiting for.
What is rv?
rv is an R package manager that takes a fundamentally different approach from traditional tools:
- Declarative — You describe the desired state of your project, not the steps to get there
- Written in Rust — Compiled for speed (25x faster than alternatives in benchmarks)
- Holistic resolution — Resolves the entire dependency tree before installing anything
- Lock at install time — Captures all installation information when it happens, not retroactively
# Install rv (Linux/macOS)
curl -sSL https://raw.githubusercontent.com/A2-ai/rv/refs/heads/main/scripts/install.sh | bash
# Or with Homebrew (macOS)
brew tap a2-ai/homebrew-tap
brew install rv- 1
-
The curl installer is the universal method — works on both Linux and macOS, places the
rvbinary on yourPATH. - 2
-
Homebrew install on macOS — preferred if you already use Homebrew; handles updates via
brew upgrade rv.
The Problem with Iterative Installation
If you’ve used R long enough, you’ve probably experienced this frustration. Traditional R package management (including renv) follows an iterative pattern:
# The workflow we all know too well
install.packages("dplyr")
install.packages("ggplot2")
# ... more packages ...
renv::snapshot() # Capture state after the factI’ve run into two issues with this approach more times than I can count:
- Incompatible versions — Packages installed at different times may have conflicting dependencies. How many times have you had a working project break after updating one package?
- Lost context — By the time you snapshot, information about how packages were installed is lost. Where did that package come from again?
rv’s Declarative Approach
With rv, you declare what you want in a rproject.toml file:
[project]
name = "my-analysis"
r_version = "4.5"
repositories = [
{ alias = "PPM", url = "https://packagemanager.posit.co/cran/latest" },
]
dependencies = [
"dplyr",
"ggplot2",
"tidyr",
]- 1
-
Pin the R version —
rvwill warn if the active R version does not match. - 2
- Posit Package Manager (PPM) serves pre-compiled binaries, making installs significantly faster than building from source.
- 3
-
Declare only direct dependencies;
rvresolves the full transitive dependency tree automatically.
Then synchronize your project:
rv syncThat’s it. Seriously. rv resolves the complete dependency tree, ensures compatibility, installs everything, and creates a lockfile — all in one atomic operation. The first time I ran this, I was genuinely surprised at how fast it was.
Quick Start
Initialize a New Project
rv init my-project
cd my-projectThis creates the following structure:
my-project/
├── rv/
│ ├── library/
│ ├── scripts/
│ │ ├── activate.R
│ │ └── rvr.R
│ └── .gitignore
├── .Rprofile
├── rproject.toml
└── rv.lock
Add Dependencies
Edit rproject.toml or use the CLI:
rv add dplyr ggplot2 tidyverseCheck What Will Happen
Before installing, preview the changes:
rv planSynchronize
rv syncKey Commands
| Command | Description |
|---|---|
rv init |
Initialize a new project |
rv sync |
Synchronize packages with config |
rv add <pkg> |
Add package to config and sync |
rv plan |
Preview what sync would do |
rv upgrade |
Update packages (ignores lockfile) |
rv tree |
Show dependency tree |
rv migrate renv |
Migrate from renv project |
rv vs renv: A Comparison
| Feature | renv | rv |
|---|---|---|
| Installation | Iterative | Declarative |
| Lock timing | After installation (snapshot) | At installation |
| Compatibility check | None (trust the process) | Full tree resolution |
| Written in | R | Rust |
| Speed | Slower | ~25x faster |
| Config format | R code + JSON lockfile | TOML + lockfile |
Migrating from renv
If you have an existing renv project:
rv migrate renvThis converts your renv.lock to an rv configuration.
The uv Parallel: Modern Package Management
As someone who works in both R and Python, I find the parallels between rv and Python’s uv striking — and clearly intentional:
| Concept | Python (uv) | R (rv) |
|---|---|---|
| Config file | pyproject.toml |
rproject.toml |
| Lock file | uv.lock |
rv.lock |
| Sync command | uv sync |
rv sync |
| Add package | uv add |
rv add |
| Written in | Rust | Rust |
| Philosophy | Declarative, fast | Declarative, fast |
Both tools share the same vision, and honestly, it’s refreshing to see R catching up:
- Speed through Rust — Compiled performance beats interpreted languages
- Declarative configuration — Describe the end state, not the steps
- Reproducibility first — Lockfiles capture everything needed to recreate the environment
- Modern developer experience (DX) — Clear CLI, helpful error messages, predictable behavior
A Modern R Workflow: rig + rv
Here’s where things get really exciting for me. Combining rv with rig (the R installation manager) gives you a workflow that finally feels on par with modern Python development:
1. Manage R Versions with rig
# Install rig (macOS)
brew tap r-lib/rig
brew install rig
# Or Linux
curl -L https://rig.r-lib.org/rig-linux-latest.tar.gz | sudo tar xz -C /usr/local
# List available R versions
rig available
# Install a specific version
rig add 4.5
# Set default version
rig default 4.5
# List installed versions
rig list- 1
- Show all R versions available for download — useful for picking the right release before installing.
- 2
- Download and install R 4.5 — isolated from other installed versions, no system pollution.
- 3
-
Make R 4.5 the active version returned by
RandRscripton the command line. - 4
- Show all locally installed R versions and which is currently the default.
2. Manage Packages with rv
# Initialize project with current R version
rv init my-analysis
# rv automatically detects R version from rig
cat rproject.toml
# [project]
# r_version = "4.5"
# ...3. The Complete Workflow
# 1. Ensure you have the right R version
rig add 4.5
rig default 4.5
# 2. Create a new project
rv init data-analysis
cd data-analysis
# 3. Add your dependencies
rv add tidyverse arrow DBI
# 4. Start working
R
# > library(tidyverse) # Just works!- 1
-
Install R 4.5 if not already present — safe to re-run;
rigskips the download if the version is already installed. - 2
-
Scaffold
rproject.tomlpre-filled with the current R version — ready to declare dependencies immediately. - 3
-
Add packages to
rproject.tomland resolve + install them in one step — no separateinstall.packages()+renv::snapshot()cycle.
Project-Specific R Versions
The r_version field in rproject.toml ensures teammates use the correct R version:
[project]
name = "critical-analysis"
r_version = "4.4" # Pin to R 4.4.xWith rig, switching is trivial:
rig switch 4.4
rv syncAdvanced Configuration
Specify Package Sources
dependencies = [
"dplyr",
{ name = "mypackage", git = "https://github.com/org/mypackage.git", tag = "v1.0.0" },
{ name = "internal", repository = "Internal" },
]
repositories = [
{ alias = "PPM", url = "https://packagemanager.posit.co/cran/latest" },
{ alias = "Internal", url = "https://internal.company.com/r-packages" },
]Binary vs Source
rv intelligently handles binary and source installations, with options to configure per-package:
dependencies = [
{ name = "arrow", source = true }, # Force source compilation
]Configure Compilation
[project.packages_env_vars.arrow]
ARROW_WITH_S3 = "ON"
ARROW_WITH_GCS = "ON"
[project.configure_args]
sf = ["--with-proj-share=/usr/local/share/proj"]Why This Matters for Reproducibility
Having worked in pharma, I know how critical reproducibility is — it’s not just nice to have, it’s a regulatory requirement. The combination of declarative package management (rv) and explicit R version management (rig) solves the two problems I’ve spent countless hours debugging:
- “Works on my machine” → Lockfile captures exact package versions and sources
- “Which R version?” →
r_versionin config +rigfor installation
For regulated environments (pharma, finance, etc.), this is transformative:
# Clone a project
git clone https://github.com/org/clinical-analysis
cd clinical-analysis
# Set up exact R version
rig add $(grep r_version rproject.toml | cut -d'"' -f2)
# Restore exact package environment
rv sync
# ✅ Identical environment, guaranteed- 1
-
Extract the
r_versionstring fromrproject.tomlwithgrep/cutand pass it directly torig add— one command installs the exact R version required by the project. - 2
- Install all packages from the lockfile at the exact recorded versions — deterministic, no network guessing.
Getting Started Today
Install rig for R version management:
brew tap r-lib/rig && brew install rigInstall rv for package management:
curl -sSL https://raw.githubusercontent.com/A2-ai/rv/refs/heads/main/scripts/install.sh | bashCreate your first project:
rv init my-project cd my-project rv add tidyverse rv sync
Conclusion
I’m genuinely excited about rv. It represents a paradigm shift in R package management — from iterative, reactive tooling to declarative, proactive workflows. Combined with rig for R version management, it finally gives us a modern development experience on par with what Python developers have enjoyed with uv and pyenv.
The R ecosystem is evolving, and I’m here for it. Tools like rv, rig, and air (the new R formatter) are bringing modern development practices to R. If you value reproducibility, speed, and developer experience like I do, give rv a try. I think you’ll be as impressed as I was.