A fast R package and project manager, written in Rust.
uvr brings uv-style project management to R: a uvr.toml manifest, a reproducible uvr.lock lockfile, and a per-project isolated library. Packages install from pre-built P3M binaries by default — no compilation, no waiting — with automatic fallback to CRAN source. R versions are managed per-project with no sudo required.
-
Linux / MacOS
curl -fsSL https://raw.githubusercontent.com/nbafrank/uvr/main/install.sh | sh -
Windows
irm https://raw.githubusercontent.com/nbafrank/uvr/main/install.ps1 | iex
Here's a following demo of uvr:
$ uvr init my-analysis
$ uvr add ggplot2 dplyr tidymodels
$ uvr sync # installs from lockfile, idempotent
$ uvr run analysis.R(Checksum-verified install to ~/.local/bin; Windows and other options under Installation.)
Prefer working from the R console? The uvr R package wraps the CLI for use from R/RStudio/Positron — no terminal needed:
pak::pak("nbafrank/uvr-r")
library(uvr)
init() # uvr init
add("ggplot2") # uvr add ggplot2
sync() # uvr sync
run("analysis.R") # uvr run analysis.RR has several package management tools — renv, pak, rv, rig — each solving a different slice of the problem. After 10+ years of R development, the workflow I kept wanting was the one uv brought to Python: a single tool that handles the full lifecycle, from installing R itself to adding packages to reproducible installs in CI, with no configuration sprawl.
Here is how existing tools compare and where the gaps are:
- renv — the de-facto standard for reproducibility. It snapshots an existing library into a lockfile, but it does not pin R versions ("renv tracks, but doesn't help with, the version of R used") and it works library-first: the lockfile records what your library already has rather than driving what gets installed. Install speed is a property of your mirror, not of renv — pointed at a binary repo like P3M it is fast (see the benchmarks below).
- pak — fast parallel installs and good system dependency detection. It does have lockfiles (
pak::lockfile_create()/pak::lockfile_install(), aimed at CI), but no R version management, and it is an installer rather than a project workflow — in practice paired with renv, not a replacement for it. - rv — the closest prior art: Rust-based, declarative, fast, with P3M binaries,
rv run,rv sysdeps, andrv sync --lockedfor CI. It selects among the R versions already installed on the machine — including onesrigput there — but does not install R itself, which is the gapuvrcloses. - rig — excellent R version manager. No package management or lockfile. Per its own FAQ it cannot install R without admin permissions.
- pixi — conda-based multi-language environment manager. Supports R via conda-forge, but packages come from conda-forge rather than CRAN/Bioconductor/P3M natively. Language-agnostic by design; not R-first.
- rix — Nix-based, with extreme reproducibility including system-level dependencies. Right tool if you need bit-for-bit reproducibility across machines. Requires Nix; a different philosophy than a fast pragmatic workflow.
uvr is the combination of all of the above in one tool, with a single config file (uvr.toml) and a single lockfile (uvr.lock). The design goals are:
- One tool, one config — no juggling renv + rig + pak.
uvr.tomldeclares both the R version and package dependencies. - Lockfile-first —
uvr.lockis the source of truth.uvr syncis always reproducible and idempotent. - Fast by default — P3M pre-built binaries on macOS, Windows, and Linux; source fallback only when needed.
- R version management built in —
uvr r install,uvr r use,uvr r pinwork the same wayuv pythondoes, because needing a separate tool for this is friction. - CI-native —
uvr sync --frozenis a first-class command, not an afterthought.
If you are happy with renv + rig, that is a perfectly good setup. uvr is for people who want the uv experience in R.
| uvr | renv | pak | rv | rig | pixi | |
|---|---|---|---|---|---|---|
| Declarative manifest | Y | Y† | Y† | Y | - | Y |
| Lockfile | Y | Y | Y | Y | - | Y |
| R version selection / pinning | Y | - | - | Y | Y | Y |
| Installs R itself | Y | - | - | - | Y | Y |
| Run scripts in isolated env | Y | Y | - | Y | - | Y |
| CRAN packages | Y | Y | Y | Y | - | Y* |
| Bioconductor packages | Y | Y | Y | Y | - | Y* |
| GitHub packages | Y | Y | Y | Y | - | - |
| Pre-built binaries (P3M) | Y | - | Y | Y | - | - |
| System dep detection (Linux) | Y | - | Y | Y‡ | - | Y |
| CI mode (fail on stale lock) | Y | Y | - | Y | - | Y |
| No admin rights required | Y | Y | Y | Y | -** | Y |
| Standalone CLI (no R required) | Y | - | - | Y | Y | Y |
| Windows support | Y | Y | Y | Y | Y | Y |
* pixi installs R packages from conda-forge, not CRAN/Bioconductor directly.
** Per rig's own FAQ, rig cannot install R without admin permissions.
† Via DESCRIPTION-based workflow, not a dedicated manifest format.
‡ Per rv sysdeps' own help, coverage is currently Ubuntu/Debian.
Install wall time (empty library, index caches warm). All tools use P3M as CRAN mirror. Median of 5 runs on Apple Silicon (arm64), R 4.6.0, uvr 0.4.6. pak was not installed on the bench machine for this run; container numbers including pak are on the website.
| Scenario | Packages | uvr sync | renv | install.packages |
|---|---|---|---|---|
| jsonlite | 1 | 0.52s | 0.56s | 2.95s |
| ggplot2 | 17 | 0.51s | 0.61s | 5.1s |
| tidyverse | 100 | 0.57s | 0.81s | 14.92s |
uvr pre-resolves dependencies into a lockfile (
uvr lock); onlyuvr sync(install) is timed. The other tools resolve dependencies inline. renv uses its default global cache (symlinks).Reproduce on your own machine:
bash benchmarks/bench.sh. Reproduce in a clean container:bash benchmarks/run-in-docker.shbuildsbenchmarks/Dockerfileand runs the bench inside it. The Dockerfile pins R version, debian base, Rust toolchain, and the CRAN-mirror PPM snapshot — so numbers from a CI run today are directly comparable to a CI run a month from now and to a local docker-build by anyone who wants to verify the published numbers (per #40). The same image runs on every tag push via.github/workflows/benchmark.yml; the workflow uploadsbench-results.jsonas an artifact and surfaces the meta block in the GH Actions step summary.
- Fast — parallel downloads, native binary extraction, no R process overhead
- Reproducible —
uvr.lockis the source of truth;uvr syncis always idempotent - Project-isolated — every project gets its own
.uvr/library/, never touching system R - Full R version management —
uvr r install 4.4.2,uvr r use >=4.3,uvr r pin 4.4.2 - CRAN + Bioconductor + GitHub —
uvr add DESeq2 --bioc,uvr add user/repo@main - Standalone scripts — declare dependencies in a
# /// scriptheader anduvr run script.Ranywhere, no project needed - CI-ready —
uvr sync --frozenfails fast if the lockfile is stale; respectsNO_COLOR - Cross-platform — macOS, Linux, and Windows with pre-built binaries for all three
- Written in Rust — single static binary, no R or Python required to install
curl -fsSL https://raw.githubusercontent.com/nbafrank/uvr/main/install.sh | shThis auto-detects your platform, downloads the binary, verifies the SHA256 checksum, and installs to ~/.local/bin. Override the install directory with UVR_INSTALL_DIR:
curl -fsSL https://raw.githubusercontent.com/nbafrank/uvr/main/install.sh | UVR_INSTALL_DIR=/usr/local/bin shYou can quick install on Windows as well with the following Powershell command:
irm https://raw.githubusercontent.com/nbafrank/uvr/main/install.ps1 | iexDownload the latest release for your platform from GitHub Releases:
# macOS (Apple Silicon)
curl -fsSL https://github.com/nbafrank/uvr/releases/latest/download/uvr-aarch64-apple-darwin.tar.gz | tar xz
sudo mv uvr /usr/local/bin/
# macOS (Intel)
curl -fsSL https://github.com/nbafrank/uvr/releases/latest/download/uvr-x86_64-apple-darwin.tar.gz | tar xz
sudo mv uvr /usr/local/bin/
# Linux (x86-64)
curl -fsSL https://github.com/nbafrank/uvr/releases/latest/download/uvr-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv uvr /usr/local/bin/
# Linux (ARM64)
curl -fsSL https://github.com/nbafrank/uvr/releases/latest/download/uvr-aarch64-unknown-linux-gnu.tar.gz | tar xz
sudo mv uvr /usr/local/bin/On Windows, download uvr-x86_64-pc-windows-msvc.zip from the releases page and add uvr.exe to your PATH.
The companion R package can install the binary for you:
# Install the R package from GitHub (uvr-r is not on CRAN yet)
pak::pak("nbafrank/uvr-r")
# or: remotes::install_github("nbafrank/uvr-r")
# Download and install the uvr binary
uvr::install_uvr()# Pre-built binary
yay -S uvr-bin
# Or build from source
yay -S uvrPackages maintained by @novica. See uvr and uvr-bin on the AUR.
From source (requires Rust)
cargo install --git https://github.com/nbafrank/uvr# Create a new project
mkdir my-project && cd my-project
uvr init --r-version ">=4.3.0"
# Add packages (CRAN, Bioconductor, GitHub)
uvr add ggplot2 dplyr
uvr add DESeq2 --bioc
uvr add tidymodels@>=1.0.0
uvr add user/repo@main
uvr add 'user/monorepo@main#subdirectory=packages/nestedPkg'
# Install everything from the lockfile
uvr sync
# Run a script in the isolated environment
uvr run analysis.R -- --input data.csv
# See what you have
uvr treeGitHub package directories also propagate through supported DESCRIPTION
Remotes: entries, including owner/repo/subdir@ref and
owner/repo:subdir@ref. This traversal follows the source chain: only a
package already selected from a manifest Git source can introduce another
remote source; ordinary registry packages cannot inject remote URLs. Bound
aliases and subdirectory targets fail rather than falling back to a registry
or to the repository root.
| Command | Description |
|---|---|
uvr init [name] |
Create uvr.toml and .uvr/library/ in the current directory |
uvr add <pkg...> |
Add packages, update manifest + lockfile, install |
uvr remove <pkg...> |
Remove packages from manifest and re-lock |
uvr sync |
Install all packages from the lockfile |
uvr sync -v |
Show the resolved install plan first — each package's source and whether it installs from binary or source |
uvr sync --frozen |
Like sync, but fail if the lockfile is stale (CI mode) |
uvr sync --no-binary |
Build everything from source, ignoring pre-built binaries |
uvr update [pkg...] |
Upgrade packages to latest allowed versions |
uvr update --dry-run |
Show what would change without installing |
uvr lock |
Re-resolve all deps and update uvr.lock without installing |
uvr lock --upgrade |
Upgrade all packages to their latest allowed versions |
uvr tree |
Show the dependency tree |
uvr tree --depth 1 |
Show only direct dependencies |
uvr run [script.R] |
Run a script (or interactive R) with the project library active |
uvr run --with pkg |
Run with extra packages available (not added to manifest) |
uvr run script.R |
Run a standalone script from its inline # /// script dependency header — outside any project |
uvr activate |
Print how to activate the project in your shell (source .uvr/activate) |
uvr r install <ver> |
Download and install a specific R version to ~/.uvr/r-versions/ (override the location with --install-dir) |
uvr r install devel |
Install a rolling channel — devel or next, rebuilt continuously and marked [unstable] (not reproducible; don't pin one) |
uvr r list |
Show installed R versions |
uvr r list --all |
Show all available R versions (fetched from the portable build index) |
uvr r use <ver> |
Set R version constraint in uvr.toml |
uvr r pin <ver> |
Write exact version to .r-version |
uvr export |
Export lockfile to renv.lock format |
uvr export -o renv.lock |
Export to a file |
uvr import |
Import packages from an renv.lock file |
uvr import --lock |
Import and immediately resolve + install |
uvr upgrade |
Update uvr itself to the latest GitHub release (alias: uvr self-update) |
uvr doctor |
Diagnose environment issues (R, build tools, project status) |
uvr completions <shell> |
Generate shell completions (bash, zsh, fish, powershell) |
uvr cache clean |
Remove all cached package downloads |
uvr cache clean --package <name> |
Remove cache entries for specific packages (repeatable, comma-separated) |
uvr cache clean --r-version <minor> |
Remove extracted-package entries built for an R minor version (e.g. 4.5) |
A script can declare its own dependencies in a header comment and run
anywhere — no project, no uvr.toml, no lockfile:
# /// script
# dependencies = [
# "jsonlite",
# "praise",
# ]
# ///
cat(praise::praise(), "\n")$ cd /anywhere && uvr run analysis.R
> Installing 2 package(s): 2 binary
v Installed 2 package(s) in 1.75s
You are epic!The dependencies install into a cached environment keyed by the dependency set, so the second run of that script — or any other script wanting the same packages — starts immediately. Nothing is written next to the script.
The header is the R analogue of Python's PEP 723
inline script metadata, which uv run uses. It must start at column zero,
may follow a shebang or banner comment, and takes plain package names today
(version constraints, Bioconductor and git sources are planned). A malformed
or duplicated header is an error naming the file and the problem, never
silently ignored.
Scripts run isolated from any project you happen to be standing in: the
project library, its .r-version pin, and its .Rprofile are all bypassed,
so a script behaves the same wherever it is invoked from.
A script can also carry a shebang and run as a plain executable:
#!/usr/bin/env -S uvr run
# /// script
# dependencies = ["praise"]
# ///
cat(praise::praise(), "\n")$ chmod +x hooray
$ ./hooray
You are wondrous!The -S flag needs GNU coreutils 8.30+ on Linux; macOS and the BSDs have
supported it for years.
Prefer working in a plain R console over prefixing everything with uvr run?
Activate the project and a bare R or Rscript uses it:
source .uvr/activate # bash, zsh, sh
source .uvr/activate.fish # fish
. .uvr/activate.ps1 # PowerShell
R # uses the project's R and .uvr/library/
deactivate # restore your shelluvr init writes these files (uvr activate --write-shim recreates them).
They contain no paths: each one asks uvr to recompute the environment as
it is sourced, so changing the project's R version with uvr r use or
uvr r pin never leaves a stale activation behind.
To show the project name in your prompt while activated — off by default:
# uvr.toml
[activate]
prompt = trueor per-shell, which overrides the manifest either way:
export UVR_ACTIVATE_PROMPT=1 # or 0 to opt out of a project that opts inGenerate and install completions for your shell:
# Zsh
uvr completions zsh > ~/.zfunc/_uvr
# Bash
uvr completions bash > /etc/bash_completion.d/uvr
# Fish
uvr completions fish > ~/.config/fish/completions/uvr.fish
# PowerShell
uvr completions powershell > $HOME\Documents\PowerShell\Completions\uvr.ps1uvr can install and manage multiple R versions without sudo or admin rights:
# Install R 4.4.2
uvr r install 4.4.2
# See what's available
uvr r list --all
# Set project constraint (writes to uvr.toml)
uvr r use ">=4.3.0"
# Pin exact version (writes .r-version file)
uvr r pin 4.4.2R versions are installed to ~/.uvr/r-versions/ and managed independently of any system R installation. uvr downloads portable, relocatable R builds from the rstudio/r-builds project (cdn.posit.co/r): each is a self-contained archive that detects its own location at runtime — no system-wide install, no admin/sudo, and no post-install patching. This makes it ideal for corporate and university environments where users cannot install software system-wide.
# GitHub Actions example
- name: Install uvr
run: |
curl -fsSL https://github.com/nbafrank/uvr/releases/latest/download/uvr-x86_64-unknown-linux-gnu.tar.gz | tar xz
sudo mv uvr /usr/local/bin/
- name: Install R
run: uvr r install 4.4.2
- name: Install packages (frozen = fail if lockfile is stale)
run: uvr sync --frozen
- name: Run tests
run: uvr run tests/run_tests.Rmy-project/
├── uvr.toml # manifest (commit this)
├── uvr.lock # lockfile (commit this)
├── .r-version # optional exact R pin (commit this)
└── .uvr/
└── library/ # isolated package library (.gitignore this)
[project]
name = "my-project"
r_version = ">=4.3.0"
[dependencies]
ggplot2 = ">=3.0.0"
dplyr = "*"
DESeq2 = { bioc = true }
myPkg = { git = "user/repo", rev = "main" }
nestedPkg = { git = "user/monorepo", rev = "main", subdirectory = "packages/nestedPkg" }
[dev-dependencies]
testthat = "*"Generated or imported git entries may also carry exact = true, which preserves an explicit DESCRIPTION PackageName= alias and requires the fetched DESCRIPTION Package: field to match the manifest dependency name.
On Linux, uvr sync automatically checks for missing system libraries and
prints the install command for your distro's package manager (apt-get,
dnf, zypper, or apk):
! Missing system dependencies for 2 package(s):
textshaping requires: libharfbuzz-dev, libfribidi-dev
ragg requires: libfreetype6-dev, libpng-dev
Install with: sudo apt-get install -y libharfbuzz-dev libfribidi-dev libfreetype6-dev libpng-dev
Pass --install-system-deps (or set UVR_INSTALL_SYSREQS=1) and uvr runs
the commands itself, showing each one and where it came from before
anything executes as root. Requirements are resolved from the
r-system-requirements
rules vendored into uvr, cross-checked against Posit's sysreqs API when
reachable.
Run uvr doctor to check your setup:
> uvr doctor
Platform
v OS / architecture macos/aarch64
v P3M binary packages available
R installations
v R 4.5.3 ~/.uvr/r-versions/4.5.3/bin/R - managed
v R 4.4.2 ~/.uvr/r-versions/4.4.2/bin/R - managed
-> active 4.5.3 ~/.uvr/r-versions/4.5.3/bin/R
Build tools
v cargo (Rust toolchain) found
v Xcode command line tools found
v Homebrew found
Project
v Manifest uvr.toml
v Lockfile 42 package(s), R 4.5.3
Cache
- 166 file(s), 204.6 MB
v No issues found
| Platform | Binary packages | Source install | R version management |
|---|---|---|---|
| macOS ARM64 (Apple Silicon) | P3M | Y | Y (R 4.1.0+) |
| macOS x86-64 | P3M | Y | Y (R 4.1.0+) |
| Linux x86-64 (glibc ≥ 2.34) | P3M (Ubuntu, Debian, RHEL, openSUSE) | Y | Y |
| Linux ARM64 (glibc ≥ 2.34) | P3M (Ubuntu, Debian, RHEL, openSUSE) | Y | Y |
| Linux (musl / Alpine) | source | Y | Y |
| Windows x86-64 | P3M | Y (with Rtools) | Y (R 4.1.0+, no admin required) |
P3M binary packages are sourced from Posit Package Manager. R itself is installed from the portable, relocatable builds published by rstudio/r-builds on Posit CDN — manylinux_2_34 tarballs on glibc Linux (requires glibc ≥ 2.34; excludes Ubuntu 20.04, RHEL 8, Debian 11), musllinux_1_2 on Alpine, ad-hoc-signed .tar.gz on macOS (R 4.1.0+), and .zip on Windows (R 4.1.0+). The portable Linux builds bundle their own libraries but expect ca-certificates and fontconfig (plus ttf-dejavu on Alpine) to be present on the host.
uvr is shaped by the people who use it and report back. Special thanks to:
- @B-Nilson — a steady stream of field reports and requests that became core behavior: cache-preserving R switches (#85), filtered cache cleaning (#92), and more.
- @bsirak — the trampoline and symlink integration RFC (#109).
- @gdevenyi — a systematic 46-issue audit of the entire codebase (#127–#172), with file-and-line precision, that drove the v0.4.1 and v0.4.2 fix batches (and a code contribution on top).
- @pat-s — the Alpine/musl system-requirements groundwork, and candid feedback that improved how this project is run.
- @hongyuanjia — suggested building on Posit's r-builds (#96), which became the foundation of the current R install backend.
- @zorbax — the precise diagnosis of the macOS GNU-tar install failure (#125).
And to everyone who has filed an issue, tested a fix, or suggested a direction — thank you; the last several releases were built from your reports.
uvr is free and MIT-licensed. If it saves you time, you can support its development on Ko-fi.
MIT — see LICENSE.
