library(rix)
rix(
r_ver = "4.4.2",
r_pkgs = c("dplyr", "ggplot2"),
ide = "rstudio",
project_path = ".",
overwrite = TRUE
)4 Reproducible Development Environments with rix
4.1 Introduction
Now that we have Nix installed, and our IDE configured we can actually tackle the reproducibility puzzle.
4.1.1 The Reproducibility Challenge
Reproducibility in research and data science exists on a continuum. At one end, authors might only describe their methods in prose. Moving along the spectrum, they might share code, then data, and finally what we call a computational environment: the complete set of software required to execute an analysis.
Even when researchers share code and data, they rarely specify the full software stack: the exact version of R, all package versions, and crucially, the system-level dependencies. Yet differences in any of these can lead to divergent results from the same code.
Tools like {renv} address part of the puzzle: they capture R package versions in a lockfile. But {renv} does not manage the R version itself (you need rig for that), and neither handles system libraries. If {sf} requires GDAL 3.0 but your system has 2.4, {renv} can’t help. And if your project uses both R and Python? Now you’re coordinating multiple package managers, each with its own configuration.
4.1.2 Component Closures: The Nix Approach
This is where Nix shines. Nix deploys component closures: when you install a package, Nix also installs all its dependencies, their dependencies, and so on. Think of it like packing for a trip—traditional package managers assume you’ll find essentials at your destination, while Nix packs everything you need.
As the original Nix paper explains:
The idea is to always deploy component closures: if we deploy a component, then we must also deploy its dependencies, their dependencies, and so on. Since closures are self-contained, they are the units of complete software deployment.
This means when you install {sf} through Nix, you automatically get the correct versions of GDAL, GEOS, and PROJ—no manual system configuration needed.
4.1.3 The Polyglot Challenge
Modern data science is increasingly polyglot. Research shows that data scientists use, on average, nearly two programming languages in their work, with R and Python being the most common combination. Python dominates machine learning, R excels at statistical modelling, and Julia offers high-performance numerics. Projects increasingly combine these strengths.
This creates a reproducibility challenge: a project using R, Python, and Quarto requires coordinating multiple package managers. Nix solves this by providing a unified framework for all languages and system tools.
4.1.4 Enter rix
However, Nix has a steep learning curve. Its functional programming language can be daunting for researchers focused on their analysis, not system administration.
{rix} bridges this gap. It’s an R package that generates Nix expressions from intuitive R function calls. You describe what you want, and {rix} figures out how to express it in Nix. The workflow is simple:
- Declare your environment using
rix() - Build the environment using
nix-build - Use the environment using
nix-shell
This chapter covers everything you need to know to create project-specific, reproducible development environments for your R projects.
4.2 Transitioning to Nix-Managed R
Now that Nix is installed, I strongly recommend uninstalling any system-wide R installation and removing the packages in your user library (typically found in ~/R on Linux or ~/Library/R on macOS). From this point forward, let Nix handle everything. If you are using Windows, you can keep your Windows-specific R installation, since Nix will not interfere with it (remember, Nix is installed inside WSL and Positron will automatically load Nix environments installed in WSL as well).
If you are not ready to take this step, you can continue using your local R library. The {rix} package makes efforts not to interfere with your existing setup, and the .Rprofile it generates helps keep Nix environments isolated. However, for the cleanest experience and to avoid subtle conflicts, I recommend fully committing to Nix.
4.2.1 Bootstrapping {rix} without a local R installation
But if R is uninstalled, how do you use {rix} to generate environments? The answer lies in the temporary shells we saw in the previous chapter. Use Nix itself to bootstrap a temporary R session with {rix} available.
Running the following line in a terminal will drop you into an interactive R session:
nix-shell -p R rPackages.rixThis gives you a temporary shell with R and {rix} ready to use. From here, you can generate project-specific Nix expressions.
For example, navigate to an empty directory for a new project:
mkdir my-project
cd my-projectThen start R:
RLoad {rix} and generate an expression:
library(rix)
rix(
date = "2025-04-11",
r_pkgs = c("dplyr", "ggplot2"),
ide = "none",
project_path = ".",
overwrite = TRUE
)This writes a default.nix file to your project directory. The expression defines a shell with R, {dplyr}, and {ggplot2} as they were on the 11th of April 2025 on CRAN. The ide argument is set to "none" because we configured Positron in the previous chapter rather than having Nix manage an IDE.
I recommend saving this code in a file called gen-env.R in your project directory. If you later need to add packages or change the R version, simply edit gen-env.R, run it to regenerate default.nix, and rebuild with nix-build. This keeps your environment definition readable and versioned alongside your project.
{rix}
If the {rix} syntax is new to you, remember that you can use pkgctx to generate LLM-ready context (as mentioned in the introduction). The {rix} repository includes a .pkgctx.yaml file you can feed to your LLM to help it understand the package’s API. You can also generate your own context file:
nix run github:b-rodrigues/pkgctx -- r github:ropensci/rix > rix.pkgctx.yamlWith this context, your LLM can help you write correct rix() calls, even if you’ve never used the package before. You can do so for any package hosted on CRAN, GitHub, or local .tar.gz files.
You can now exit this temporary session (type q() in R, then exit in the shell) and build your new environment:
nix-buildOnce built, use nix-shell to enter your project’s environment. This workflow of bootstrapping {rix} via a temporary shell, generating a default.nix, and then building it is the pattern you will follow for every new project.
This is also the reason why there is no Python version of {rix}: you can bootstrap a Python environment using {rix} in the same way, by only temporarily having the R interpreter available through the Nix shell.
4.3 The rix() Function
The rix() function is the heart of the package. It generates a default.nix file—a Nix expression that defines your development environment. Here are its main arguments:
r_ver: the version of R you need (or usedateinstead)date: a date corresponding to a CRAN snapshotr_pkgs: R packages to install from CRAN/Bioconductorsystem_pkgs: system tools likequartoorgitgit_pkgs: R packages to install from GitHublocal_r_pkgs: local.tar.gzpackages to installtex_pkgs: TexLive packages for literate programmingide: which IDE to configure ("rstudio","code","positron","none"). Since we’ve configured Positron in the previous chapter, you’ll want to set this to"none". If you want to use RStudio, then set it to"rstudio". This will install RStudio using Nix and make it available from the development shell. If you set it to"code"or"positron", this will then install VS Code or Positron using Nix.project_path: where to save thedefault.nixfileoverwrite: whether to overwrite an existingdefault.nix
Let’s create our first environment. Suppose you need R with {dplyr} and {ggplot2}:
This generates two files:
default.nix: The Nix expression defining your environment.Rprofile: Created byrix_init()(called automatically), this file prevents conflicts with any system-installed R packages
The .Rprofile is important: it ensures that packages from your user library don’t get loaded into the Nix environment, and it redefines install.packages() to throw an error, because you should never install packages that way in a Nix environment.
4.4 Choosing an R Version or Date
The r_ver argument (or alternatively date) controls which version of R and which package versions you’ll get. Here’s a summary of the options:
r_ver / date |
Intended Use | R Version | Package Versions |
|---|---|---|---|
"latest-upstream" |
New project, versions don’t matter | Current/previous | Up to 6 months old |
"4.4.2" (or similar) |
Reproduce old project or start new | Specified version | Up to 2 months old |
date = "2024-12-14" |
Precise CRAN snapshot | Current at that date | Exact versions from date |
"bleeding-edge" |
Develop against latest CRAN | Always current | Always current |
"frozen-edge" |
Latest CRAN, manual updates | Current at generation | Current at generation |
"r-devel" |
Test against R development version | R-devel | Always current |
To see which R versions are available:
available_r()To see which dates are available for snapshotting:
available_dates()4.4.1 Using dates vs versions
Using a specific date is often the best choice for reproducibility. When you specify a date, you get the exact state of CRAN on that day:
rix(
date = "2024-12-14",
r_pkgs = c("dplyr", "ggplot2"),
ide = "none",
project_path = ".",
overwrite = TRUE
)I find that this is often easier and clearer than using an R version. Be careful though, as using a date doesn’t reflect the state of PyPi on that date. So if you also need Python packages, the versions of packages for Python that will be provided are the ones available from nixpkgs on that day (but more on this later).
4.4.2 The rstats-on-nix fork
When you use a specific R version or date (rather than "latest-upstream"), {rix} uses our rstats-on-nix fork of nixpkgs rather than the upstream repository. This fork:
- Snapshots CRAN more frequently
- Offers newer R releases faster than the official channels
- Includes many fixes, especially for Apple Silicon (even though as time goes by, this is becoming much rarer, because Nix is getting better and more support for Apple Silicon)
4.5 Installing R Packages
4.5.1 From CRAN/Bioconductor
The simplest case—just list package names in r_pkgs:
rix(
r_ver = "4.4.2",
r_pkgs = c("dplyr", "ggplot2", "tidyr", "readr"),
ide = "none",
project_path = "."
)Both CRAN and Bioconductor packages can be specified this way.
4.5.2 Installing archived versions
Need a specific old version of a package? Use the @ syntax:
rix(
r_ver = "4.2.1",
r_pkgs = c("dplyr@0.8.0", "janitor@1.0.0"),
ide = "none",
project_path = "."
)This will install {dplyr} version 0.8.0 from the CRAN archives. Note that archived packages are built from source, which may fail for packages requiring compilation. Thus I recommend you use a date on which that specific version of {dplyr} was current.
4.5.3 Installing from GitHub
For packages on GitHub, use the git_pkgs argument with a list containing the package name, repository URL, and commit hash:
rix(
r_ver = "4.4.2",
r_pkgs = c("dplyr", "ggplot2"),
git_pkgs = list(
list(
package_name = "housing",
repo_url = "https://github.com/rap4all/housing/",
commit = "1c860959310b80e67c41f7bbdc3e84cef00df18e"
)
),
ide = "none",
project_path = "."
)Always specify a commit hash, not a branch name. This ensures reproducibility: branch names can change, but commits are immutable.
If the R package lives in a subfolder of the repository, append the subfolder to the URL:
git_pkgs = list(
package_name = "BPCells",
repo_url = "https://github.com/bnprks/BPCells/r", # Note the /r suffix
commit = "16faeade0a26b392637217b0caf5d7017c5bdf9b"
)Note that this will install the package from source, so if it’s a package that requires specific system dependencies, you will need to specify them manually (Nix takes care of this for you for packages that are available through nixpkgs, but not for ad-hoc packages from other sources).
4.5.4 Installing local packages
For local .tar.gz archives, place them in the same directory as your default.nix and use local_r_pkgs:
rix(
r_ver = "4.3.1",
local_r_pkgs = c("mypackage_1.0.0.tar.gz"),
ide = "none",
project_path = "."
)Just like with packages from Git, note that this will install the package from source, so if it’s a package that requires specific system dependencies, you will need to specify them manually (Nix takes care of this for you for packages that are available through nixpkgs, but not for ad-hoc packages from other sources).
4.5.5 Why NOT to use install.packages()
It’s crucial to understand: never call install.packages() from within a Nix environment. Here’s why:
- Declarative environments: If you install packages imperatively, your
default.nixno longer matches your actual environment. - Leaking packages: Packages installed via
install.packages()go to your user library, not the Nix environment. They’ll be visible to all Nix shells, breaking isolation. - Reproducibility: The whole point of Nix is that your environment is fully defined by the
default.nix. Ad-hoc installations defeat this.
Instead, add packages to your rix() call and rebuild the environment.
4.6 System Tools and TexLive
4.6.1 Adding system packages
Need command-line tools like Quarto or Git? Add them via system_pkgs:
rix(
r_ver = "latest-upstream",
r_pkgs = c("quarto"),
system_pkgs = c("quarto", "git"),
ide = "none",
project_path = "."
)Note that here we install both the R {quarto} package and the quarto command-line tool—they’re different things!
To find available packages, search at search.nixos.org.
4.6.2 TexLive for literate programming
For PDF output from Quarto, R Markdown, or Sweave, you need TexLive packages:
rix(
r_ver = "latest-upstream",
r_pkgs = c("quarto"),
system_pkgs = "quarto",
tex_pkgs = c("amsmath", "framed", "fvextra"),
ide = "none",
project_path = "."
)This installs the scheme-small TexLive distribution plus the specified packages.
4.6.3 Python and Julia integration
{rix} can also add Python or Julia to your environment:
rix(
date = "2025-02-17",
r_pkgs = "ggplot2",
py_conf = list(
py_version = "3.12",
py_pkgs = c("polars", "great-tables")
),
ide = "none",
project_path = "."
)This creates a polyglot environment with R, Python, and the specified packages for each.
4.6.4 Installing Python packages with uv (impure)
Not all Python packages (or versions of packages) are available through Nix. Unlike CRAN, PyPI doesn’t get automatically mirrored—individual packages must be packaged by volunteers. If a Python package you need isn’t in nixpkgs, you can use uv as an escape hatch.
This approach is also useful when collaborating with colleagues who use uv but haven’t adopted Nix yet. uv is 10–100x faster than pip and generates a lock file for improved reproducibility.
The idea is to install uv in your shell (but not Python or Python packages through Nix):
rix(
date = "2025-02-17",
r_pkgs = "ggplot2",
system_pkgs = c("uv"),
ide = "none",
project_path = "."
)Then use uv from within your shell. We recommend:
- Specify Python packages in a
requirements.txtfile with explicit versions (e.g.,scanpy==1.11.4) - Set up a shell hook to automatically configure the virtual environment
Here’s a complete example with a shell hook:
rix(
date = "2025-02-17",
r_pkgs = "ggplot2",
system_pkgs = c("uv"),
shell_hook = "
if [ ! -f pyproject.toml ]; then
uv init --python 3.13.5
fi
uv add --requirements requirements.txt
alias python='uv run python'
",
ide = "none",
project_path = "."
)After running nix-shell, uv initialises a Python project with the specified version and installs packages from requirements.txt. This happens each time you enter the shell, but uv caches everything so it’s nearly instant after the first run.
4.6.4.1 Troubleshooting wheel issues
When using wheels (pre-compiled Python packages), you may encounter errors like:
ImportError: libstdc++.so.6: cannot open shared object file
This happens because wheels expect certain libraries in certain locations. Add this to your shell hook to fix it:
shellHook = ''
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath (with pkgs; [
zlib gcc.cc glibc stdenv.cc.cc
])}":$LD_LIBRARY_PATH
# ... rest of your hook
'';If this seems complicated: yes, it is. This is exactly the kind of problem Nix aims to solve. When possible, prefer Python packages included in nixpkgs.
4.7 Building and Using Environments
4.7.1 Building with nix-build
Once you have a default.nix, build the environment:
nix-buildThis downloads/builds all required packages and creates a result symlink in your project directory. The result file prevents the environment from being garbage-collected.
4.7.2 Entering with nix-shell
To use the environment interactively:
nix-shellYou’ll drop into a shell where R and all your packages are available. Type R to start an R session. If you’re using RStudio (and specified ide = "rstudio" in the call to rix()) then type rstudio to launch RStudio. If instead you configured Positron, (or any other IDE), open Positron, and open the project folder that contains the default.nix (make sure you also have an .envrc there for direnv to load the environment directly).
You can even run scripts directly without entering the shell:
nix-shell default.nix --run "Rscript analysis.R"This is quite useful on CI/CD platforms.
4.7.3 Pure shells for complete isolation
By default, nix-shell can still see programs installed on your system. This is quite important to understand: building the environment happens in an isolated, hermetic sandbox, but when inside a shell, isolation is more porous and it is possible to use other systems tools. For example, you don’t need to install git inside the Nix development environment: you can just keep using the git executable already available on your system.
But it is possible to run the environment with increased isolation:
nix-shell --pureThis hides everything not explicitly included in your environment.
4.7.4 Garbage collection
Nix never deletes old packages automatically. To clean up:
- Delete the
resultsymlink that will appear in your project’s folder after callingnix-build - Run
nix-store --gc
This removes all packages that are no longer referenced by any environment.
4.8 Converting renv Projects
If you have existing projects using {renv}, the renv2nix() function can help you migrate:
renv2nix(
renv_lock_path = "path/to/project/renv.lock",
project_path = "path/to/new_nix_project"
)4.8.1 Recommended workflow
- Copy the
renv.lockfile to a new, empty folder - Run
renv2nix()pointing to that folder - Build the environment with
nix-build
Do not convert in the same folder as the original {renv} project—the generated .Rprofile will conflict with {renv}’s .Rprofile.
4.8.2 Caveats
- Package versions may not match exactly due to how Nix handles snapshotting
- If the
renv.locklists an old R version but recent packages, use theoverride_r_verargument to specify a more appropriate R version
4.9 Summary
Let’s step back and recap why we have gone through all this trouble.
Traditional tools like {renv} or Python’s venv only capture part of the reproducibility puzzle. They track package versions but not the language version itself, nor system-level dependencies like GDAL or Java. This means your project can still break on a different machine, or even on your own machine after a system update.
Nix solves this by managing everything: R, Python, all packages, and all system dependencies. When you define an environment with {rix}, you get a complete, self-contained specification that anyone can use to recreate the exact same environment, on any machine, at any point in the future.
4.9.1 Quick Reference: Starting a New Project
Here is the workflow you will follow for every new project:
Create an empty folder for your project:
mkdir my-project cd my-projectWrite a
gen-env.Rfile with your environment definition:library(rix) rix( date = "2025-04-11", r_pkgs = c("dplyr", "ggplot2"), ide = "none", # or "rstudio" if you prefer project_path = ".", overwrite = TRUE )Add a
.envrcfile (only needed for Positron or VS Code, skip if using RStudio):use nix mkdir $TMPBootstrap
{rix}and generatedefault.nix:nix-shell -p R rPackages.rixThen in R:
source("gen-env.R")Exit R with
q()and the shell withexit.Build the environment:
nix-build # you could also run `direnv allow` to allow direnv to load the environment # automatically and build the environment on first useStart working: Open the folder in Positron (which will load the environment via
direnv), or runnix-shellfollowed byrstudioif you setide = "rstudio".
That’s it. Your project is now reproducible. Anyone with Nix installed can clone your repository, run nix-build, and get the exact same environment you have.