15

install.packages() returns a warning if a package cannot be installed (for instance, if it is unavailable); for example:

install.packages("notapackage")

(EDIT: I'd like to throw an error regardless of the reason the package cannot be installed, not just this example case of a missing package).

I am running the install.packages command in a script, and I would like it to trigger a proper error and exit execution. I don't see an obvious option inside install.packages for handling this behavior. Any suggestions?

cboettig
  • 12,377
  • 13
  • 70
  • 113
  • Check with `available.packages()` first? – Dirk Eddelbuettel Oct 07 '14 at 20:22
  • Terrible terrible terrible solution with text matching `install.package2 <- function(...){ifelse(any(grepl("Warning", capture.output(install.packages(...)))), 1, 0)}` and `cake.installer("cake"); cake.installer("lmtest")` – Vlo Oct 07 '14 at 20:28
  • @Vlo yeah, my thoughts too. Perhaps we can do better with `withCallingHandlers()`... – cboettig Oct 07 '14 at 20:37

6 Answers6

13

Having just solved this for myself, I noted that install.packages() results in calling library(thepackage) to see it its usable.

I made a R script that installs the given packages, uses library on each to see if its loadable, and if not calls quit with a non-0 status.

I call it install_packages_or_die.R

#!/usr/bin/env Rscript

packages = commandArgs(trailingOnly=TRUE)

for (l in packages) {

    install.packages(l, dependencies=TRUE, repos='https://cran.rstudio.com/');

    if ( ! library(l, character.only=TRUE, logical.return=TRUE) ) {
        quit(status=1, save='no')
    }
}

Use it like this in your Dockerfile. I'm grouping them in useful chunks to try and make reasonable use of Docker build cache.

ADD install_packages_or_die.R /

RUN Rscript --no-save install_packages_or_die.R profvis devtools memoise

RUN Rscript --no-save install_packages_or_die.R memoise nosuchpackage

Disclaimer: I do not consider myself an R programmer at all currently, so there may very well be better ways of doing this.

Cameron Kerr
  • 1,725
  • 16
  • 23
  • I have been fighting this all day. Thank you for including this! Wouldn't another way to do it be something like: RUN R -e "install_packages_or_die <- function (pkgs, lib=.libPaths(),repos='http://cran.rstudio.com/') { for (l in pkgs) { install.packages(l, dependencies=TRUE, repos=repos); if ( ! library(l, character.only=TRUE, logical.return=TRUE) ) { quit(status=1, save='no') } } } install_packages_or_die (pkgs =c('profviz','devtools'))" ``` – Aphoid Feb 17 '21 at 22:30
  • the RUN R -e "..." would let you embed the function in the Dockerfile instead of needing to add a file and another layer. – Aphoid Feb 18 '21 at 00:06
  • This will work in many circumstances, but won't be sufficient if there is a possibility that a given package already exists prior to attempting installation, such as for the case where you are trying to update the package version. – dpritch Aug 29 '22 at 21:05
  • @dpritch Good point. I think this whole question is really trying to solve the wrong problem. A better question I think would be "What's the R equivalent of tools like Python's pip (and virtual environments)". I'm no R expert, far from it, but I think the answer might be 'packrat' https://community.rstudio.com/t/how-do-you-isolate-packages-environments-for-r-data-products-in-production/13004 – Cameron Kerr Sep 01 '22 at 00:32
6

The R function WithCallingHandlers() lets us handle any warnings with an explicitly defined function. For instance, we can tell the R to stop if it receives any warnings (and return the warning message as an error message).

withCallingHandlers(install.packages("notapackage"),
                    warning = function(w) stop(w))

I am guessing that this is not ideal, since presumably a package could install successfully but still throw a warning; but haven't encountered that case. As Dirk suggests, testing require for the package is probably more robust.

Dirk Eddelbuettel
  • 360,940
  • 56
  • 644
  • 725
cboettig
  • 12,377
  • 13
  • 70
  • 113
  • That may do it. On the missing package we get `stop()` invoked, next question is whether a failed build (missing headers, ...) wll get caught. – Dirk Eddelbuettel Oct 07 '14 at 20:57
  • Yup, missing headers etc all throw a generic warning: e.g. `Warning message: In install.packages("XML") : installation of package 'XML' had non-zero exit status` occurs on `install.packages("XML")` on `debian-r-base` which lacks `libxml2` – cboettig Oct 07 '14 at 21:02
  • `tryCatch` works here in place of `withCallingHandlers` as well – C. Hammill Sep 14 '18 at 18:28
3

try to install then look for warnings to stop the execution and return an error. also calls library() , just in case !

 install_or_fail <- function(package_name){ 

   tryCatch({install.packages(package_name, dependencies = TRUE) 
         library(package_name)}, 
         error = function(e){ print(e) }, 
         warning = function(w){
           catch <-
             grepl("download of package .* failed", w$message) ||
             grepl("(dependenc|package).*(is|are) not available", w$message) ||
             grepl("installation of package.*had non-zero exit status", w$message) ||
             grepl("installation of one or more packages failed", w$message)
           if(catch){ print(w$message)
             stop(paste("installation failed for:",package_name ))}}
         )

 }

inspired by : https://github.com/eddelbuettel/littler/blob/master/inst/examples/install2.r

Mnl
  • 787
  • 8
  • 9
  • This is very helpful, thanks. Just one comment: if the goal is to throw errors on failures, wouldn't the `error = function(e){ print(e) }` argument suppress those failures? I think you can just leave that argument out. – dpritch Aug 29 '22 at 21:01
  • I added a similar solution based on this one to this post: https://stackoverflow.com/a/73534778/5518304 – dpritch Aug 29 '22 at 21:44
0

Expanding on the quick comment:

R> AP <- available.packages()
R> "notapackage" %in% AP[,1]      # expected to yield FALSE
[1] FALSE
R> "digest" %in% AP[,1]           # whereas this should be TRUE
[1] TRUE
R> 
Dirk Eddelbuettel
  • 360,940
  • 56
  • 644
  • 725
  • I picked a bad minimal example. We want a solution that throws an error when the install.packages() fails regardless of the cause (cannot compile, etc), not just the case of a missing package. – cboettig Oct 07 '14 at 20:28
  • See `help(install.packages)` and check for return value. Methinks they promise you an unconditional `NULL` so I fear _no mas_ unless you preempt (as I suggested) or rewrite... – Dirk Eddelbuettel Oct 07 '14 at 20:31
  • Correct, `install.packages` returns NULL no matter what. I do not follow how I can preempt the various reasons package install fails... – cboettig Oct 07 '14 at 20:34
  • 1
    The question I answered was concerned with the example of a *missing* package. I answered that. You since changed the scope and parameters of your question and now have a harder question on hand. From the top of my head you could *follow* the installation attempt with `require(pkg)` and report its status. Sucky, but you gotta deal with the installer given ... – Dirk Eddelbuettel Oct 07 '14 at 20:35
0

Building off of Cameron Kerr's answer, here's another solution that would work in a Dockerfile (or at a unix command line) without needing to add an additional file/layer:

RUN R -e " \
   install_packages_or_die <- function (pkgs, repos='http://cran.rstudio.com/') { \
   for (l in pkgs) {  install.packages(l, dependencies=TRUE, repos=repos); \
       if ( ! library(l, character.only=TRUE, logical.return=TRUE) ) { \
          stop ('ERROR: failed installing requested package \'',l,'\'') } } } ; \
   install_packages_or_die (pkgs= c('mime')); "

RUN R -e " \
   install_packages_or_die <- function (pkgs, repos='http://cran.rstudio.com/') { \
   for (l in pkgs) {  install.packages(l, dependencies=TRUE, repos=repos); \
       if ( ! library(l, character.only=TRUE, logical.return=TRUE) ) { \
          stop ('ERROR: failed installing requested package \'',l,'\'') } } } ; \
   install_packages_or_die (pkgs= c('packagedoesnotexist')); "

Note: sometimes packages install dependencies after the requested package failed, and you'll still have to search the log to see what the actual error was.

Aphoid
  • 351
  • 3
  • 8
0

I've added another version that essentially tweaks @mnl's solution (which is in turn inspired by @dirk-eddelbuettel's install_packages2 function) to collect and report all of the package build failures.

# `install.packages` unfortunately does not throw an error if package
# installation fails but rather a warning. So we check the warnings and promote
# the appropriate ones to errors. The regex conditions are taken from the
# `install_packages2` function in
# https://github.com/eddelbuettel/littler/blob/master/inst/examples/install2.r
warns <- character(0L)
withCallingHandlers(
  expr = {
    install.packages(c("pk1", "pkg2", "etc"))
  }, warning = function(w) {
    catch <- (
      grepl("download of package .* failed", w$message)
      || grepl("(dependenc|package).*(is|are) not available", w$message)
      || grepl("installation of package.*had non-zero exit status", w$message)
      || grepl("installation of one or more packages failed", w$message)
    )
    if (catch) {
      warns <<- c(warns, w$message)
      invokeRestart("muffleWarning")
    }
  }
)
if (length(warns) >= 1L) {
  msg <- paste(warns, collapse = "\n")
  stop(msg)
}
dpritch
  • 1,149
  • 13
  • 15