import re import os import datetime from .python import PythonBuildPack class RBuildPack(PythonBuildPack): """ Setup R for use with a repository This sets up R + RStudio + IRKernel for a repository that contains: 1. A `runtime.txt` file with the text: r--- Where 'year', 'month' and 'date' refer to a specific date snapshot of https://mran.microsoft.com/timemachine from which libraries are to be installed. 2. An optional `install.R` file that will be executed at build time, and can be used for installing packages from both MRAN and GitHub. The `r-base` package from Ubuntu apt repositories is used to install R itself, rather than any of the methods from https://cran.r-project.org/. """ @property def runtime(self): """ Return contents of runtime.txt if it exists, '' otherwise """ if not hasattr(self, '_runtime'): runtime_path = self.binder_path('runtime.txt') try: with open(runtime_path) as f: self._runtime = f.read().strip() except FileNotFoundError: self._runtime = '' return self._runtime @property def checkpoint_date(self): """ Return the date of MRAN checkpoint to use for this repo Returns '' if no date is specified """ if not hasattr(self, '_checkpoint_date'): match = re.match(r'r-(\d\d\d\d)-(\d\d)-(\d\d)', self.runtime) if not match: self._checkpoint_date = False else: self._checkpoint_date = datetime.date(*[int(s) for s in match.groups()]) return self._checkpoint_date def detect(self): """ Check if current repo should be built with the R Build pack super().detect() is not called in this function - it would return false unless a `requirements.txt` is present and we do not want to require the presence of a `requirements.txt` to use R. Instead we just check if runtime.txt contains a string of the form `r---
` """ # If no date is found, then self.checkpoint_date will be False # Otherwise, it'll be a date object, which will evaluate to True return bool(self.checkpoint_date) def get_path(self): """ Return paths to be added to the PATH environment variable. The RStudio package installs its binaries in a non-standard path, so we explicitly add that path to PATH. """ return super().get_path() + [ '/usr/lib/rstudio-server/bin/' ] def get_build_env(self): """ Return environment variables to be set. We want libraries to be installed in a path that users can write to without needing root. This is set via the `R_LIBS_USER` environment variable, so we set that here. """ return super().get_build_env() + [ # This is the path where user libraries are installed ('R_LIBS_USER', '${APP_BASE}/rlibs') ] def get_packages(self): """ Return list of packages to be installed. We install a base version of R, and packages required for RStudio to be installed. """ return super().get_packages().union([ 'r-base', # For rstudio 'psmisc', 'libapparmor1', 'sudo', 'lsb-release' ]) def get_build_scripts(self): """ Return series of build-steps common to all R repositories All scripts here should be independent of contents of the repository. This sets up: - A directory owned by non-root in ${R_LIBS_USER} for installing R packages into - RStudio - R's devtools package, at a particular frozen version (determined by MRAN) - IRKernel - nbrsessionproxy (to access RStudio via Jupyter Notebook) """ rstudio_url = 'https://download2.rstudio.org/rstudio-server-1.1.419-amd64.deb' # This is MD5, because that is what RStudio download page provides! rstudio_checksum = '24cd11f0405d8372b4168fc9956e0386' # Via https://www.rstudio.com/products/shiny/download-server/ shiny_url = 'https://download3.rstudio.org/ubuntu-14.04/x86_64/shiny-server-1.5.7.907-amd64.deb' shiny_checksum = '78371a8361ba0e7fec44edd2b8e425ac' # Version of MRAN to pull devtools from. devtools_version = '2018-02-01' # IRKernel version - specified as a tag in the IRKernel repository irkernel_version = '0.8.11' return super().get_build_scripts() + [ ( "root", r""" mkdir -p ${R_LIBS_USER} && \ chown -R ${NB_USER}:${NB_USER} ${R_LIBS_USER} """ ), ( "root", # Install RStudio! r""" curl --silent --location --fail {rstudio_url} > /tmp/rstudio.deb && \ echo '{rstudio_checksum} /tmp/rstudio.deb' | md5sum -c - && \ dpkg -i /tmp/rstudio.deb && \ rm /tmp/rstudio.deb """.format( rstudio_url=rstudio_url, rstudio_checksum=rstudio_checksum ) ), ( "root", # Install Shiny Server! r""" curl --silent --location --fail {url} > {deb} && \ echo '{checksum} {deb}' | md5sum -c - && \ dpkg -i {deb} && \ rm {deb} """.format( url=shiny_url, checksum=shiny_checksum, deb='/tmp/shiny.deb' ) ), ( "root", # Set paths so that RStudio shares libraries with base R # install. This first comments out any R_LIBS_USER that # might be set in /etc/R/Renviron and then sets it. r""" sed -i -e '/^R_LIBS_USER=/s/^/#/' /etc/R/Renviron && \ echo "R_LIBS_USER=${R_LIBS_USER}" >> /etc/R/Renviron """ ), ( "${NB_USER}", # Install nbrsessionproxy r""" pip install --no-cache-dir nbrsessionproxy==0.7.0 && \ jupyter serverextension enable nbrsessionproxy --sys-prefix && \ jupyter nbextension install --py nbrsessionproxy --sys-prefix && \ jupyter nbextension enable --py nbrsessionproxy --sys-prefix """ ), ( "${NB_USER}", # Install a pinned version of IRKernel and set it up for use! r""" R --quiet -e "install.packages('devtools', repos='https://mran.microsoft.com/snapshot/{devtools_version}', method='libcurl')" && \ R --quiet -e "devtools::install_github('IRkernel/IRkernel', ref='{irkernel_version}')" && \ R --quiet -e "IRkernel::installspec(prefix='$NB_PYTHON_PREFIX')" """.format( devtools_version=devtools_version, irkernel_version=irkernel_version ) ), ( "${NB_USER}", # Install shiny library r""" R --quiet -e "install.packages('shiny', repos='https://mran.microsoft.com/snapshot/{}', method='libcurl')" """.format( self.checkpoint_date.isoformat() ) ), ] def get_assemble_scripts(self): """ Return series of build-steps specific to this repository We set the snapshot date used to install R libraries from based on the contents of runtime.txt, and run the `install.R` script if it exists. """ mran_url = 'https://mran.microsoft.com/snapshot/{}'.format( self.checkpoint_date.isoformat() ) assemble_scripts = super().get_assemble_scripts() + [ ( "root", # We set the default CRAN repo to the MRAN one at given date # We set download method to be curl so we get HTTPS support r""" echo "options(repos = c(CRAN='{mran_url}'), download.file.method = 'libcurl')" > /etc/R/Rprofile.site """.format(mran_url=mran_url) ), ( # Not all of these locations are configurable; log_dir is "root", r""" install -o ${NB_USER} -g ${NB_USER} -d /var/log/shiny-server && \ install -o ${NB_USER} -g ${NB_USER} -d /var/lib/shiny-server && \ install -o ${NB_USER} -g ${NB_USER} /dev/null /var/log/shiny-server.log && \ install -o ${NB_USER} -g ${NB_USER} /dev/null /var/run/shiny-server.pid """ ), ] installR_path = self.binder_path('install.R') if os.path.exists(installR_path): assemble_scripts += [ ( "${NB_USER}", "Rscript %s" % installR_path ) ] return assemble_scripts