remotes icon indicating copy to clipboard operation
remotes copied to clipboard

Issue with `safe_install_packages` setting `R_LIBS*` env vars

Open rundel opened this issue 6 years ago • 3 comments

I recently ran into an odd issue when attempting to use remotes::install_github with a lib argument to install into a non-standard library location. The conditions for this to occur are quite specific but I think it demonstrates a problematic edge case.

Conditions:

  • One globally writable system library exists and is set in .libPaths()
.libPaths()
## [1] "/usr/local/Cellar/r/3.6.1_1/lib/R/library"
  • The user library does not currently exist but env var R_LIBS_USER is defined
  • The user library is created but not added to .libPaths(), e.g.
dir.create(Sys.getenv("R_LIBS_USER"), recursive = TRUE)
  • We then attempt to install a package into the user library via
remotes::install_github("rundel/chunktest", lib=Sys.getenv("R_LIBS_USER"))

Problem:

In this case due to how safe_install_packages works and the lazy evaluation of the lib argument readr will get installed in the system library and not in the user library.

The issue as I've been able to work out is that safe_install_packages sets R_LIBS, R_LIBS_USER, and R_LIBS_SITE to paste(.libPaths(), collapse = .Platform$path.sep) and due to lazy evaluation the argument lib=Sys.getenv("R_LIBS_USER") finally evaluates in the install.packages call which is after these env vars have been overwritten, thereby giving me the wrong/unexpected value for lib.

Clearly this can be avoided by forcing evaluation of Sys.getenv("R_LIBS_USER") before passing it to one of remotes' install functions but this seems like a fairly insidious edge case that silently causes unexpected behavior.

Proposed Solution:

It's not entirely clear to me that setting these env vars is entirely necessary here, but I don't have a deep understanding of install.packages internals - but it seems like the default behavior amounts to look at lib, then the first value of .libPaths(), then R_LIBS_USER and ask about creating a personal library.

remotes' current behavior seems unnecessary since if the first entry of .libPaths() is not writable there is nothing to be done since the same directory is now also stored in R_LIBS_USER. I also have not seen anywhere that R_LIBS or R_LIBS_SITE are used but I've only looked at utils::install.packages.

rundel avatar Oct 04 '19 12:10 rundel

In general, the directory in R_LIBS_USER needs to exist before the R process starts up, creating it afterwards is basically undefined behavior.

jimhester avatar Oct 04 '19 16:10 jimhester

I’m on my phone so I can’t check the source explicitly, but isn’t creating that directory something that install.packages does in the case .libPaths() is not writable?

rundel avatar Oct 04 '19 16:10 rundel

This is what I am seeing in install.packages

if (length(lib) == 1L && !ok) {
        warning(gettextf("'lib = \"%s\"' is not writable", lib), 
            domain = NA, immediate. = TRUE)
        userdir <- unlist(strsplit(Sys.getenv("R_LIBS_USER"), 
            .Platform$path.sep))[1L]
        if (interactive()) {
            ans <- askYesNo(gettext("Would you like to use a personal library instead?"), 
                default = FALSE)
            if (!isTRUE(ans)) 
                stop("unable to install packages")
            lib <- userdir
            if (!file.exists(userdir)) {
                ans <- askYesNo(gettextf("Would you like to create a personal library\n%s\nto install packages into?", 
                  sQuote(userdir)), default = FALSE)
                if (!isTRUE(ans)) 
                  stop("unable to install packages")
                if (!dir.create(userdir, recursive = TRUE)) 
                  stop(gettextf("unable to create %s", sQuote(userdir)), 
                    domain = NA)
                .libPaths(c(userdir, .libPaths()))
            }
        }
        else stop("unable to install packages")
    }

rundel avatar Oct 07 '19 09:10 rundel