This is a guide for creating, developing and testing R packages
under a Nix Shell using R tools such as devtools, testthat and
usethis.
R is a programming language and environment for statistical
computing. I have been using R since 2003, developed some R packages
and published a few of them. I am not using it on a regular basis
anymore, but I noticed that various tools emerged over time as
de-facto tools for creating and maintaining R packages.
Nix, on the other hand, is a package manager that can be used to
provision development, testing and production environments in a
reproducible manner.
In this guide, we will use Nix to provision a development environment
for creating an R package. Such an environment can then be used by
other developers to contribute to the package. Furthermore, it can be
used for automated testing (such as on CI/CD pipelines), packaging and
even deploying solutions to production environments as it is
reproducible. It mainly helps with a huge class of "works on my
machine" kind of problems.
The most important requirement for this guide is to have Nix installed
on one's workstation. The official guide should help.
Let's start...
Nix Shell for Bootstrapping
We will use R and a particular set of packages to create our new R
package. Let's say that we do not have R installed on our workstation,
or we do not want to use the available R in this process.
A Nix Shell is like1 a terminal session where we can define
dependencies and declare environment variables that will override our
global system setup without touching it. We can then persist our Nix
Shell definition in a file and distribute it so that such terminal
session can be reproduced elsewhere with the same dependencies and
environment variables.
At this moment, we just need R with some packages to bootstrap our
package.
It is very important to understand that the R provisioned by Nix Shell
will not have access to R packages installed system-wide or to
user-specific library sites. The advantage is that we will get an R
setup only with the packages we asked for with their pinned
versions. The disadvantage is that we have to ask for an R setup with
its dependencies explicitly.
It is not that difficult, though. We need R with the following
packages:
Issue the following command to build and enter our (temporary) Nix
shell:
nix-shell --packages 'rWrapper.override{ packages = [ rPackages.devtools rPackages.testthat rPackages.usethis ]; }'
It may take some time, but we will enter a shell where we can now
launch our R session:
R
The R version may be different than your globally installed R
version. Also, check your installed packages which will most likely be
different and fewer in number compared to your existing global R
setup:
rownames(installed.packages())
Good. Let's create the package.
Initializing the R Project
usethis
has a function to do this. Assuming that you will create an
R package with the name hebele
under ./hebele
path:
usethis::create_package(
path = "./hebele",
fields = list(
Package = "hebele",
Title = "Sample R Package Project Powered by Nix",
Description = "This is a sample R package project accompanied by Nix artifacts for development and deployment purposes.",
"Authors@R" = utils::person("Vehbi Sinan", "Tunalioglu", email = "vst@vsthost.com", role = c("aut", "cre")),
URL = "https://github.com/vst/hebele",
BugReports = "https://github.com/vst/hebele/issues"
),
rstudio = FALSE,
roxygen = TRUE,
check_name = TRUE,
open = FALSE
)
We have the skeleton of our project. Let's change to the working
directory of our package:
usethis::proj_activate("./hebele")
In the next subsections, we will add some flesh to our project:
README.md File
Create a README.md
file:
usethis::use_readme_md()
Note that this function will open a README.md
file template using
your $EDITOR
for you to edit it. Change it or do it later.
NEWS.md File
Create a NEWS.md
file:
usethis::use_news_md()
Note that this function will open a NEWS.md
file template using
your $EDITOR
for you to edit it. Change it or do it later.
LICENSE File and Descriptor
Create a LICENSE
file as per MIT license:
usethis::use_mit_license()
This will update your DESCRIPTION
file, create LICENSE
and
LICENSE.md
files and add LICENSE.md
to .Rbuildignore
file.
First R File and Definition
Let's create an R file that contains an R function to be exported by
our package:
usethis::use_r("greeting.R")
Note that this function will open an empty R file using your $EDITOR
for you to edit it. Let's add the following content to it:
#' Prepares greeting string.
#'
#' @param name Whom to greet.
#' @return A greeting.
#' @examples
#' hello()
#' hello("Birader")
#'
#' @export
hello <- function (name = "World") {
paste0("Hello ", name, "!")
}
First R Test File and Test Definition
To create a test file, issue the following statement:
usethis::use_test("test-greeting")
Note that this function will open an R test file template using your
$EDITOR
for you to edit it. Let's add the following content to it:
test_that("hello works as expected", {
expect_equal(hello(), "Hello World!")
expect_equal(hello("birader"), "Hello birader!")
})
Package Check
Now, we can use devtools
to check our package:
devtools::check(".")
Note that you may get a NOTE about the
NEWS.md
file contents. This
is normal as ourNEWS.md
does not have proper content yet and you
will have to deal with it when you are about to release your
package.
Git Setup
Let's initialize the project as a Git repository and make our first
commit. You may wish to use conventional commits which you will
later benefit much from:
usethis::use_git(message = "chore: init repository")
Adding Dependencies
By now, you should be able to load the package and call your
definitions:
devtools::load_all(".")
hello()
hello("birader")
Let's say, we want to create a new function, namely helloStranger
which greets a person with a random name. For this, we would like to
use randomNames library. But, we do not have it yet.
First, exit the R. Then, exit the Nix Shell. Create a new Nix Shell
with the added dependency:
nix-shell --packages 'rWrapper.override{ packages = [ rPackages.devtools rPackages.randomNames rPackages.testthat rPackages.usethis ]; }'
Then, run R:
R
Activate the project if you did not run R from within the package
directory:
usethis::proj_activate("./hebele")
Let's add the package to our project (DESCRIPTION
to be specific):
usethis::use_package("randomNames")
Now, add the following function to our greeting.R
file (you can
directly edit ./R/greeting.R
file, or simply run
usethis::use_r("greeting.R")
):
#' Prepares greeting string for a stranger.
#'
#' @return A greeting message with some random first/last name.
#' @examples
#' hello_stranger()
#'
#' @export
hello_stranger <- function () {
hello(randomNames::randomNames(which.names="both", name.order="first.last", name.sep=" "))
}
Check your project to see if everything is in order:
devtools::check(".")
Finally, you commit your changes:
usethis::use_git(message = "feat: add hello_stranger function")
Saving Nix Shell in a File
We better put our Nix Shell definition into a file and commit it to
our Git repository.
The name of the file is shell.nix
, and the content is:
{ pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-23.11") { }
, ...
}:
let
## Development dependencies:
devDependencies = [
pkgs.rPackages.devtools
pkgs.rPackages.testthat
pkgs.rPackages.usethis
];
## Production (package) dependencies:
libDependencies = [
pkgs.rPackages.randomNames
];
## Our R package with development and production dependencies:
thisR = pkgs.rWrapper.override {
packages = devDependencies ++ libDependencies;
};
in
pkgs.mkShell {
buildInputs = [
## Include our R package with its dependencies:
thisR
## Any additional packages we want in our Nix Shell:
pkgs.git
];
}
However, we need to exclude it from the R build process. Add the
following line to .Rbuildignore
:
shell.nix
From now onwards, we do not need to specify any arguments on
nix-shell
command as nix-shell
command will find and read
shell.nix
in the directory it is executed:
nix-shell
One thing to be noted: Every time we add a new dependency to our R
package, we need to add it to our shell.nix
first, and then, add it
to our package using usethis::use_package
function. Likewise, if we
need a development dependency, we will add it to our shell.nix
.
TODOs
The main motivation behind this post was to demonstrate how to
bootstrap an R package using a Nix Shell, nothing more. I left these
out, but we could have done following on top of what we have done
here:
- Setup a linter for static analysis
- Setup a code formatter for checking and correcting the format of our code
- Setup R Language Server
- Setup GitHub Actions to build and check the package
- Setup GitHub Actions for automated releases2
Footnotes
-
It is much more than that, indeed. I just said so for the sake
of our purpose. ↩ -
Release Please would be nice, but it does not officially
support R language yet. However, it may happen one day. ↩
Top comments (0)