Some thoughts on melding R with lower level languages and package management

Background

I like plotting with ggplot2. It makes more sense to me than the other plotting systems. These notes arose while binding readCon and potlib for cuh2vizR. However, for best practices, my fastMatMR project should be consulted.

Idiosyncracies

  • I find it odd that R packages commit so much generated code
    • Can be simplified with a CI bot

Repo setup

pixi with renv turns out to be surprisingly robust. I normally begin with:

1echo ".pixi/" > .gitignore
2gibo dump R C++ | cat >> .gitignore
3pixi init
4pixi add radian r-renv compilers
5git add .
6git commit -m "PRJ: Let there be light"

The rest of the preliminaries can take place within the context of renv and pixi shell. I tend to leave LICENSE and other templates to usethis.

renv package development

This is fairly straightforward, but takes a while to run, from the radian terminal (after pixi-shell).

1renv::init()
2install.packages(c("devtools")) # can take a while
3usethis::create_package(".") # from $GITROOT

Now we can start with the rest of the templates.

1usethis::use_mit_license()
2usethis::use_readme_rmd()

Warning

CRAN doesn’t like fixed dependencies, see the discussion here, and often for developing packages one might not need renv though it still makes sense if one has multiple development machines. This turned out to be something I regretted, though pixi is still useful.

Stylistic decisions

Always a good idea to lint and style from the get-go, so:

1install.packages(c("lintr", "styler"))

Also remember to add .pixi/ to .Rbuildignore otherwise local checks will parse the entire contents of the rather substantial folder.

Pre-commit hooks

For my packages, I tend to use a pre-commit-hook configured with .pre-commit-config.yaml:

 1repos:
 2- repo: https://github.com/pre-commit/pre-commit-hooks
 3  rev: v4.4.0
 4  hooks:
 5  - id: trailing-whitespace
 6    exclude: ^(.lintr)
 7  - id: end-of-file-fixer
 8  - id: check-yaml
 9  - id: check-added-large-files
10- repo: https://github.com/pre-commit/mirrors-clang-format
11  rev: v16.0.6
12  hooks:
13  - id: clang-format

Which can be used via:

1# Check
2pipx run pre-commit run -a
3# Install
4pipx run pre-commit install

Also worth noting is the CI configuration for the same especially useful for contributors, .github/workflows/pre-commit.yml:

 1name: pre-commit
 2on:
 3  pull_request:
 4  push:
 5    branches: [main]
 6jobs:
 7  pre-commit:
 8    runs-on: ubuntu-latest
 9    steps:
10    - uses: actions/checkout@v3
11    - uses: actions/setup-python@v3
12      with:
13        python-version: '3.11'
14    - uses: pre-commit/action@v2.0.3

However, sometimes pre-commit for R can be flakey. In such situations consider:

1find . \( -path "./.pixi" -o -path "./renv" \) -prune -o -type f -name "*.R" -exec Rscript -e 'library(styler); style_file("{}")' \;
2Rscript -e 'library(lintr); lintr::lint_package(".")'

In any case, always remember to run renv::snapshot() after an install.packages call.

ROpenSci package check

Warning

This is really slow locally, but required in the renv workflow. Still annoying to run locally!

We need to install this with a non-CRAN source:

1options (repos = c (
2        ropenscireviewtools = "https://ropensci-review-tools.r-universe.dev",
3        CRAN = "https://cloud.r-project.org"
4    ))
5install.packages("pkgcheck")

When its finally done, add the CI configuration.

 1name: ROpenSci pkg-check
 2on:
 3  push:
 4    branches:
 5      - main
 6  pull_request:
 7    branches:
 8      - main
 9jobs:
10  pkgcheck:
11    runs-on: ubuntu-latest
12    steps:
13      - uses: actions/checkout@v3
14        with:
15          submodules: "recursive"
16          fetch-depth: 0
17      - name: pkgcheck
18        uses: ropensci-review-tools/pkgcheck-action@v1.0.0

Using cpp11

Even after usethis::use_package("cpp11") one must link to it explicitly in the DESCRIPITON:

1 SystemRequirements: C++17
2 Encoding: UTF-8
3 Roxygen: list(markdown = TRUE)
4 RoxygenNote: 7.2.3
5+LinkingTo:
6+    cpp11
7 Imports:
8     cpp11

One of the other things which tripped me up is that the functions are not automatically exported, and so one needs to create, in R/$PKGNAME-package.R:

1## usethis namespace: start
2#' @useDynLib your-package-name, .registration = TRUE
3## usethis namespace: end
4#' @export func-name-from-cpp11.R
5NULL

Finally, in most cases I found devtools::load_all() to be a bit insufficient, had to use the more complete:

1devtools::clean_dll()
2cpp11::cpp_register()
3devtools::document()
4devtools::load_all()

I never understood why the R community commits so many generated files, personally I like to prepend my gitignore with:

1## Generated files
2R/cpp11.R
3src/cpp11.cpp
4man/

Warning

Sadly the entire R ecosystem of checks will fail with this, so a first approximation committing everything might make some sense..

Later, a bot or CI action can be used to populate a “full” branch of the repository without muddying my nice git history. In fact, some of these are actually already part of the set of Github Actions supported by usethis::. I ended up with:

1usethis::use_github_action() ## Basic, smoke test
2usethis::use_github_action("check-standard") ## Overwrite older

I was also initially thrown by the requirement of including copies of source code. I suspect this might be bypassed by using meson as I have in cuh2vizR, unless CRAN packages are built without internet access. It would make it much easier to handle updates if meson were to handle setting up the dependencies, however, it isn’t exactly part of R-tools1.

Appeasing pkgcheck

Some thoughts:

  • No renv in .Rprofile
  • Use codemetar to get a codemeta.json
  • Add a CONTRIBUTING.md (e.g. here)
    • Not strictly necessary but I also go with a CODE_OF_CONDUCT.md (e.g. here)

Personally I needed some tex updates:

1tlmgr install collection-latexrecommended
2tlmgr install collection-fontsrecommended

Always best to include a .covrignore but when in a pinch, this will work:

1covr::package_coverage(line_exclusions=file.path(".pixi", list.files(path=".pixi", recursi
2    ve=T)))

Also useful was:

1usethis::use_build_ignore(c("newsfragments/", "pixi.*", "CONTRIBUTING.md", "CODE_OF_CONDUCT.md", ".covrignore"))

It is also important, for benchmarking and other long running vignettes, to have them cached or precomputed, as detailed in this blog post which in turn references an older ROpenSci post. Vignette images are a whole other beast, nicely covered here.

Conclusions

These were my rough notes while developing guidelines during the first release of fastMatMR. There were additional changes to my packages, mostly in terms of making them more in keeping with the R community expectations and best practices. I went through the ROpenSci process and eventually also released to CRAN, but will cover that in a follow-up.


  1. Yet. ↩︎