5 minutes
Written: 2023-11-10 18:50 +0000
Writing R Packages
Some thoughts on melding
Rwith 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
Rpackages 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
renvin.Rprofile - Use
codemetarto get acodemeta.json - Add a
CONTRIBUTING.md(e.g. here)- Not strictly necessary but I also go with a
CODE_OF_CONDUCT.md(e.g. here)
- Not strictly necessary but I also go with a
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.
Yet. ↩︎
