import { SoftwarePackage } from '@stencila/schema' import PackageGenerator from './PackageGenerator' import IUrlFetcher from './IUrlFetcher' /** * A Dockerfile generator for R packages */ export default class RGenerator extends PackageGenerator { /** * Date used to pin the CRAN mirror used */ date: string constructor (urlFetcher: IUrlFetcher, pkg: SoftwarePackage, folder?: string) { super(urlFetcher, pkg, folder) // Default to yesterday's date (to ensure MRAN is available for the date) // Set here as it is required in two methods below let date = this.package.datePublished if (!date) date = (new Date(Date.now() - 24 * 3600 * 1000)).toISOString().substring(0,10) this.date = date } // Methods that override those in `Generator`. // See that class for documentation on what each function does applies (): boolean { return this.package.runtimePlatform === 'R' } envVars (sysVersion: string): Array<[string, string]> { return [ // Set the timezone to avoid warning from Sys.timezone() // See https://github.com/rocker-org/rocker-versioned/issues/89 ['TZ', 'Etc/UTC'] ] } aptKeysCommand (sysVersion: string): string { return 'apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 51716619E084DAB9' } aptRepos (base: string): Array { let version = this.baseVersionName(base) // At time of writing, MRAN did not have an ubuntu:18.04(bionic) repo which supported R 3.4 (only bionic_3.5) // See https://cran.microsoft.com/snapshot/2018-11-05/bin/linux/ubuntu/ // So append that to the deb line if (version === 'bionic') version += '-cran35' return [ `deb https://mran.microsoft.com/snapshot/${this.date}/bin/linux/ubuntu ${version}/` ] } aptPackages (sysVersion: string): Array { let pkgs: Array = [ 'r-base' ] /** * Recurse through `softwareRequirements` and find any deb packages */ function find (pkg: any) { if (pkg.runtimePlatform !== 'R' || !pkg.softwareRequirements) return for (let subpkg of pkg.softwareRequirements) { if (subpkg.runtimePlatform === 'deb') pkgs.push(subpkg.name || '') else find(subpkg) } } find(this.package) return pkgs } stencilaInstall (sysVersion: string): string | undefined { return `apt-get update \\ && apt-get install -y zlib1g-dev libxml2-dev pkg-config \\ && apt-get autoremove -y \\ && apt-get clean \\ && rm -rf /var/lib/apt/lists/* \\ && Rscript -e 'install.packages("devtools")' \\ && Rscript -e 'source("https://bioconductor.org/biocLite.R"); biocLite("graph")' \\ && Rscript -e 'devtools::install_github("r-lib/pkgbuild")' \\ && Rscript -e 'devtools::install_github("stencila/r")'` } installFiles (sysVersion: string): Array<[string, string]> { // Copy user defined files if they exist if (this.exists('install.R')) return [['install.R', 'install.R']] if (this.exists('DESCRIPTION')) return [['DESCRIPTION', 'DESCRIPTION']] // Generate a .DESCRIPTION with valid name to copy into image const name = (this.package.name || 'unnamed').replace(/[^a-zA-Z0-9]/,'') const pkgs = this.filterPackages('R').map(pkg => pkg.name) let desc = `Package: ${name} Version: 1.0.0 Date: ${this.date} Description: Generated by Dockter ${new Date().toISOString()}. To stop Dockter generating this file and start editing it yourself, rename it to "DESCRIPTION". ` if (pkgs.length) desc += `Imports:\n ${pkgs.join(',\n ')}\n` this.write('.DESCRIPTION', desc) return [['.DESCRIPTION', 'DESCRIPTION']] } installCommand (sysVersion: string): string | undefined { if (this.exists('install.R')) { // Run the user supplied installation script return `Rscript install.R` } else if (this.exists('DESCRIPTION') || this.exists('.DESCRIPTION')) { // To keep the Dockerfile as simple as possible, download and // execute the installation-from-DESCRIPTION script. return `bash -c "Rscript <(curl -sL https://unpkg.com/@stencila/dockter/src/install.R)"` } } /** * The files to copy into the Docker image * * Copies all `*.R` files to the container */ projectFiles (): Array<[string, string]> { const rfiles = this.glob('**/*.R') return rfiles.map(file => [file, file]) as Array<[string, string]> } /** * The command to execute in a container created from the Docker image * * If there is a top-level `main.R` or `cmd.R` then that will be used, * otherwise, the first `*.R` files by alphabetical order will be used. */ runCommand (): string | undefined { const rfiles = this.glob('**/*.R') if (rfiles.length === 0) return let script if (rfiles.includes('main.R')) script = 'main.R' else if (rfiles.includes('cmd.R')) script = 'cmd.R' else script = rfiles[0] return `Rscript ${script}` } }