1

I am currently working on a continuous integration setup for R packages developed by our company. We have one Jenkins project for each R package and a corresponding library for each project.

I already defined a logic that installs all dependencies of the package into the project library. Now i want to define a check stage which basically runs

devtools::check("${PROJECT_DIR}/pkg")

but only use the project library for dependencies. I tried to use the callr package in the following manner.

callr::r(
  function(...) {
    devtools::check(...)
  ),
  args = list("${PROJECT_DIR}/pkg"),
  libpath = "${PROJECT_DIR}/lib"
)

However, the check process is still able to find packages which are not installed in libpath. Is there a way to make sure that only "${PROJECT_DIR}/lib" is used during the build stage?

So far, I have tried the following to no avail

  • callr() with the libpath argument
  • withr::with_libpaths with the new argument
  • Look through the documentation in devtools::check and R CMD BUILD for appropriate parameters
  • Use .libPaths("${JOB_DIR}/lib")

Here is a repex to explain the unexpected behavior of callr. I expect an error in line 3.

find.package("ggplot2", .libPaths()[1])
#> Error in find.package("ggplot2", .libPaths()[1]): there is no package called 'ggplot2'
callr::r(function() {  ggplot2::vars() }, libpath = .libPaths()[1])
#> named list()
find.package("ggplot2", .libPaths()[2])
#> [1] "/data/R/3.5.3/lib/R/library/ggplot2"
callr::r(function() {  ggplot2::vars() }, libpath = .libPaths()[2])
#> named list()
Gregor de Cillia
  • 7,397
  • 1
  • 26
  • 43

2 Answers2

1

Accoding to this question there is a way to archieve this with base::assign. If there is a more proper solution, I would love to hear it.

callr::r(function() {
  assign(".lib.loc", .libPaths()[1], envir = environment(.libPaths))
  ggplot2::vars() 
})
#> Error in loadNamespace(name): there is no package called ‘ggplot2’

The issues I have here are twofold

  1. It is basically a hack and can break any time if the internals of .libPaths() change
  2. I might have to modify .Library and .Library.site (internals of .libPaths()) as well in order to make sure that devtools::check is affected appropriately.
Gregor de Cillia
  • 7,397
  • 1
  • 26
  • 43
1

This might be slightly off topic, but have you considered using docker for this use case?

You can define a Dockerfile, that you reference in your Jenkinsfile, which will define a custom image for each CI job that runs. You install the packages onto the docker container, using devtools::install() within Jenkins. This container then gets tossed when the CI is done.

With this approach you don't have to worry about manually installing the packages yourself when you run your CI, and don't have to worry about conflicting namespaces across different packages.

This definitely has a higher start up cost, but I think you'll find it will be worth it in the long run for testing your R packages. Source: I also test internal R packages at my job.

sample Dockerfile

FROM docker.io/rocker/r-base

USER root

# Install packages needed for package development
RUN R -e 'install.packages(c("devtools", "rmarkdown", "testthat", "roxygen2"))'

You then reference this Dockerfile in the Jenkinsfile in order to install, test, and check the package (pipeline example below)

    agent {
        dockerfile {
            args '-u root'
        }
    }
    stages {
        stage('Install') {
            steps {
                sh 'r -e "devtools::install()"'
            }
        }
        stage('Test') {
            steps {
                sh '''
                  r -e "options(testthat.output_file = 'test-out.xml'); devtools::test(reporter = 'junit')"
                '''
                junit 'test-out.xml'
            }
        }
        stage('Check') {
           // Might need to modify expected ouput, depends on devtools version
            steps {
                sh '''
                  testOutput=$(R -e "devtools::check(args='--no-tests')" 2>&1)
                  echo "${testOutput}" | grep -q "0 errors ✔ | 0 warnings ✔ | 0 notes ✔"
                '''
            }
        }
    }
}

Paul Wildenhain
  • 1,321
  • 10
  • 12
  • Thank you very much for the input. We already use prebuilt docker images in our pipelines that include devtools and other "infrastructure packages". What I'm trying to do is find a way to cache the dependency packages on a per-project basis. Therefore, my issue here is that I'd need to update the docker images from Jenkins in order to implement what I intended. – Gregor de Cillia Apr 19 '19 at 15:22
  • Is there a reason you want to cache the package dependencies, rather than do a fresh `devtools::install()` each time? I've detected nasty bugs, before my users did, by always installing the newest versions of the packages. The downside is that my CI does take ~20 mins to install a fresh package, especially with `tidyverse` package dependencies Also, when you say "update the docker images from Jenkins" you can do that by including a custom `Dockerfile` in the same directory as you `Jenkinsfile` for the project. Please let me know if I'm misunderstanding your needs for this use case – Paul Wildenhain Apr 19 '19 at 15:29
  • It's basically just to reduce the build times. We have some packages that get updated several times a week and oftentimes the indirect rcpp dependencies lead to build times around, you said it, 20 minutes or more – Gregor de Cillia Apr 19 '19 at 15:33
  • Also, we use Jenkins for rmarkdown documents and those might trigger 10+ builds a day. – Gregor de Cillia Apr 19 '19 at 15:55
  • Ah I see, yea build times can be killer. What about creating a custom docker image for each package, each with the package dependencies pre-installed? And then reference the custom docker image in each `Jenkinsfile`? This would require more manually updates (e.g update the image when you add/remove a dependency), but it would definitely save on your build time. – Paul Wildenhain Apr 19 '19 at 16:03
  • I'd have to familiarize myself with docker before I can say if this is feasible. Currently, there is no manual work needed since the dependencies are detected from the description file (and in cases of rmarkdown from the Jenkinsfile). If there is an easy way to update the images from Jenkins, this seems like a very solid appriach. – Gregor de Cillia Apr 19 '19 at 16:35
  • I bet you could schedule a Jenkins job that runs a script daily or once a week, which re-builds your custom docker images. Then you would only need to manually update them if you added/removed dependencies, and wanted to test that immediately. Good luck! – Paul Wildenhain Apr 19 '19 at 16:42