5 minutes
Written: 2023-11-10 18:50 +0000
Updated: 2024-08-06 00:53 +0000
Writing R Packages
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-tools
1.
Appeasing pkgcheck
Some thoughts:
- No
renv
in.Rprofile
- Use
codemetar
to 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. ↩︎