Develop R Packages under Nix Shell

Vehbi Sinan Tunalioglu - Jan 9 - - Dev Community

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 ]; }'
Enter fullscreen mode Exit fullscreen mode

It may take some time, but we will enter a shell where we can now
launch our R session:

R
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

We have the skeleton of our project. Let's change to the working
directory of our package:

usethis::proj_activate("./hebele")
Enter fullscreen mode Exit fullscreen mode

In the next subsections, we will add some flesh to our project:

README.md File

Create a README.md file:

usethis::use_readme_md()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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, "!")
}
Enter fullscreen mode Exit fullscreen mode

First R Test File and Test Definition

To create a test file, issue the following statement:

usethis::use_test("test-greeting")
Enter fullscreen mode Exit fullscreen mode

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!")
})
Enter fullscreen mode Exit fullscreen mode

Package Check

Now, we can use devtools to check our package:

devtools::check(".")
Enter fullscreen mode Exit fullscreen mode

Note that you may get a NOTE about the NEWS.md file contents. This
is normal as our NEWS.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")
Enter fullscreen mode Exit fullscreen mode

Adding Dependencies

By now, you should be able to load the package and call your
definitions:

devtools::load_all(".")
hello()
hello("birader")
Enter fullscreen mode Exit fullscreen mode

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 ]; }'
Enter fullscreen mode Exit fullscreen mode

Then, run R:

R
Enter fullscreen mode Exit fullscreen mode

Activate the project if you did not run R from within the package
directory:

usethis::proj_activate("./hebele")
Enter fullscreen mode Exit fullscreen mode

Let's add the package to our project (DESCRIPTION to be specific):

usethis::use_package("randomNames")
Enter fullscreen mode Exit fullscreen mode

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=" "))
}
Enter fullscreen mode Exit fullscreen mode

Check your project to see if everything is in order:

devtools::check(".")
Enter fullscreen mode Exit fullscreen mode

Finally, you commit your changes:

usethis::use_git(message = "feat: add hello_stranger function")
Enter fullscreen mode Exit fullscreen mode

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
  ];
}
Enter fullscreen mode Exit fullscreen mode

However, we need to exclude it from the R build process. Add the
following line to .Rbuildignore:

shell.nix
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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


  1. It is much more than that, indeed. I just said so for the sake
    of our purpose. 

  2. Release Please would be nice, but it does not officially
    support R language yet. However, it may happen one day. 

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .