Commit 289c1ed0 authored by Bakaniko's avatar Bakaniko

Add liftr base and P8 logos

parent 7e591326
^.*\.Rproj$
^\.Rproj\.user$
^\.travis\.yml$
^appveyor\.yml$
^_pkgdown\.yml$
^CONDUCT\.md$
^CONTRIBUTING\.md$
* text=auto
data/* binary
src/* text=lf
R/* text=lf
inst/examples/bioc-rnaseq.bib linguist-vendored=true
.Rproj.user
.Rhistory
.RData
.Ruserdata
.DS_Store
*.tex
language: r
cache: packages
# Contributor Code of Conduct
As contributors and maintainers of this project, we pledge to respect all people who
contribute through reporting issues, posting feature requests, updating documentation,
submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for
everyone, regardless of level of experience, gender, gender identity and expression,
sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
Examples of unacceptable behavior by participants include the use of sexual language or
imagery, derogatory comments or personal attacks, trolling, public or private harassment,
insults, or other unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments,
commits, code, wiki edits, issues, and other contributions that are not aligned to this
Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed
from the project team.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by
opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the Contributor Covenant
(http:contributor-covenant.org), version 1.0.0, available at
http://contributor-covenant.org/version/1/0/0/
# Contributing to liftr
:+1::tada: First off, thanks for taking your time to contribute! :tada::+1:
You could contribute to this project by:
1. Filing a bug report or feature request in an issue.
2. Suggesting a change via a pull request.
## Issues
To file an issue about a possible bug, please try to include:
- Relevant package versions
- Necessary code and data to reproduce the issue
## Pull Requests
To suggest a change via pull requests, please:
1. Fork the repository into your GitHub account.
2. Clone the forked repository to local machine, make the changes.
3. Commit and push the changes to GitHub. Create a pull request.
Package: liftr
Type: Package
Title: Containerize R Markdown Documents
Version: 0.7.9000
Authors@R: c(
person("Nan", "Xiao", email = "me@nanx.me", role = c("aut", "cre"),
comment = c(ORCID = "0000-0002-0250-5673")),
person("Miaozhu", "Li", email = "miaozhu.li@duke.edu", role = "ctb"),
person("Tengfei", "Yin", email = "tengfei.yin@sevenbridges.com", role = "ctb")
)
Maintainer: Nan Xiao <me@nanx.me>
Description: Persistent reproducible reporting by containerization of R Markdown documents.
License: GPL-3 | file LICENSE
SystemRequirements: Docker (see <https://docs.docker.com/engine/installation/>)
VignetteBuilder: knitr
URL: https://liftr.me, https://github.com/road2stat/liftr
BugReports: https://github.com/road2stat/liftr/issues
Depends:
R (>= 3.0.2)
Imports:
yaml,
knitr,
rmarkdown,
stringr,
rstudioapi
Encoding: UTF-8
RoxygenNote: 6.0.1.9000
This diff is collapsed.
# Generated by roxygen2: do not edit by hand
export(drender)
export(lift)
export(purge_container)
export(purge_image)
export(render_docker)
importFrom(knitr,knit)
importFrom(rmarkdown,render)
importFrom(rstudioapi,getActiveDocumentContext)
importFrom(stringr,str_trim)
importFrom(yaml,as.yaml)
importFrom(yaml,yaml.load)
importFrom(yaml,yaml.load_file)
#' RStudio Addin: Containerize R Markdown Document
#'
#' RStudio addin for containerizing the current document.
#'
#' @importFrom rstudioapi getActiveDocumentContext
#'
#' @keywords internal
addin_lift = function() {
context = rstudioapi::getActiveDocumentContext()
path = normalizePath(context$'path')
lift(path)
}
#' RStudio Addin: Containerize and Render R Markdown Document with Docker
#'
#' RStudio addin for containerizing and rendering the current document.
#'
#' @importFrom rstudioapi getActiveDocumentContext
#'
#' @keywords internal
addin_lift_render_docker = function() {
context = rstudioapi::getActiveDocumentContext()
path = normalizePath(context$'path')
lift(path)
render_docker(path)
}
#' RStudio Addin: Purge Docker Image
#'
#' RStudio addin for removing the Docker image used for
#' rendering the current document.
#'
#' @importFrom rstudioapi getActiveDocumentContext
#'
#' @keywords internal
addin_purge_image = function() {
context = rstudioapi::getActiveDocumentContext()
path = normalizePath(context$'path')
path = paste0(file_dir(path), '/', file_name_sans(path), '.docker.yml')
purge_image(path)
}
#' Containerize R Markdown Documents
#'
#' @description
#' Containerize R Markdown documents. This function generates
#' \code{Dockerfile} based on the liftr metadata in the RMD document.
#'
#' @details
#' After running \link{lift}, run \link{render_docker} on the document to
#' render the Dockerized R Markdown document using Docker containers.
#' See \code{vignette('liftr-intro')} for details about the extended
#' YAML front-matter metadata format used by liftr.
#'
#' @param input Input (R Markdown) file.
#' @param use_config If \code{TRUE}, will parse the liftr metadata from
#' a YAML file, if \code{FALSE}, will parse such information from the
#' metadata section in the R Markdown file. Default is \code{FALSE}.
#' @param config_file Name of the YAML configuration file, under the
#' same directory as the input file. Default is \code{"_liftr.yml"}.
#' @param output_dir Directory to output \code{Dockerfile}.
#' If not provided, will be the same directory as \code{input}.
#'
#' @return \code{Dockerfile}.
#'
#' @export lift
#'
#' @importFrom knitr knit
#' @importFrom yaml yaml.load
#'
#' @examples
#' # copy example file
#' dir_example = paste0(tempdir(), '/liftr-minimal/')
#' dir.create(dir_example)
#' file.copy(system.file("examples/liftr-minimal.Rmd", package = "liftr"), dir_example)
#'
#' # containerization
#' input = paste0(dir_example, "liftr-minimal.Rmd")
#' lift(input)
#'
#' \dontrun{
#' # render the document with Docker
#' render_docker(input)
#'
#' # view rendered document
#' browseURL(paste0(dir_example, "liftr-minimal.html"))
#'
#' # purge the generated Docker image
#' purge_image(paste0(dir_example, "liftr-minimal.docker.yml"))}
lift = function(
input = NULL,
use_config = FALSE, config_file = '_liftr.yml',
output_dir = NULL) {
if (is.null(input))
stop('missing input file')
if (!file.exists(normalizePath(input)))
stop('input file does not exist')
if (!use_config) {
# locate YAML metadata block
doc_content = readLines(normalizePath(input))
header_pos = which(doc_content == '---')
# handling YAML blocks ending with three dots
if (length(header_pos) == 1L) {
header_dot_pos = which(doc_content == '...')
if (length(header_dot_pos) == 0L) {
stop('Cannot correctly locate YAML metadata block.
Please use three hyphens (---) as start line & end line,
or three hyphens (---) as start line with three dots (...)
as end line.')
} else {
header_pos[2L] = header_dot_pos[1L]
}
}
doc_yaml = paste(
doc_content[(header_pos[1L] + 1L):(header_pos[2L] - 1L)],
collapse = '\n')
opt_all_list = yaml.load(doc_yaml)
# liftr options handling
if (is.null(opt_all_list$liftr))
stop('Cannot find `liftr` option in file header')
opt_list = opt_all_list$liftr
}
if (use_config) {
liftrfile_path = paste0(file_dir(input), '/', config_file)
if (!file.exists(normalizePath(liftrfile_path)))
stop('The YAML config file does not exist')
opt_list = yaml.load_file(liftrfile_path)
}
# base image
liftr_from = parse_from(opt_list$from)
# maintainer's name
liftr_maintainer = parse_maintainer(opt_list$maintainer)
# maintainer's email
liftr_email = parse_email(opt_list$email)
# system dependencies
liftr_sysdeps = parse_sysdeps(opt_list$sysdeps)
# texlive
liftr_texlive = parse_texlive(opt_list$texlive)
# pandoc
liftr_pandoc = parse_pandoc(liftr_from, opt_list$pandoc)
# CRAN packages
liftr_cran = parse_cran(opt_list$cran)
# Bioconductor packages
liftr_bioc = parse_bioc(opt_list$bioc)
# remote packages
liftr_remotes = parse_remotes(opt_list$remotes)
# custom Dockerfile snippet
liftr_include = parse_include(input, opt_list$include)
# factory packages
liftr_factory = quote_str(c(
'devtools', 'knitr', 'rmarkdown', 'shiny', 'RCurl'))
# write output files
if (is.null(output_dir)) output_dir = file_dir(input)
output_dockerfile = paste0(normalizePath(output_dir), '/Dockerfile')
invisible(knit(
system.file('templates/base.Rmd', package = 'liftr'),
output = output_dockerfile, quiet = TRUE))
# remove consecutive blank lines
out = readLines(output_dockerfile)
out = sanitize_blank(out)
writeLines(out, output_dockerfile)
invisible(out)
}
#' Containerize R Markdown Documents
#'
#' Persistent reproducible reporting by
#' containerization of R Markdown documents.
#'
#' \tabular{ll}{ Package: \tab liftr\cr Type: \tab Package\cr
#' License: \tab GPL-3\cr }
#'
#' @name liftr-package
#' @docType package
#' @author
#' Nan Xiao <\email{me@@nanx.me}>
#' Miaozhu Li <\email{miaozhu.li@@duke.edu}>
#' Tengfei Yin <\email{tengfei.yin@@sevenbridges.com}>
#'
NULL
parse_from = function(from) {
if (!is.null(from)) from else
'rocker/r-base:latest'
}
parse_maintainer = function(maintainer) {
if (!is.null(maintainer)) maintainer else
stop('Cannot find `maintainer` in header')
}
parse_email = function(email) {
if (!is.null(email)) email else
stop('Cannot find `email` in header')
}
parse_sysdeps = function(sysdeps) {
if (!is.null(sysdeps))
paste(readLines(system.file(
'templates/system-deps.Rmd', package = 'liftr')),
paste(sysdeps, collapse = ' '), sep = ' ') else NULL
}
parse_texlive = function(texlive) {
if (!is.null(texlive)) {
if (texlive == TRUE) paste(
readLines(system.file(
'templates/doc-texlive.Rmd', package = 'liftr')),
collapse = '\n') else NULL
} else {
NULL
}
}
# this solves https://github.com/road2stat/liftr/issues/12
parse_pandoc = function(liftr_from, pandoc) {
if (is_from_bioc(liftr_from) | is_from_rstudio(liftr_from)) {
NULL
} else {
if (!is.null(pandoc)) {
if (pandoc == FALSE) {
NULL
} else {
paste(readLines(system.file(
'templates/doc-pandoc.Rmd', package = 'liftr')), collapse = '\n')
}
} else {
paste(readLines(system.file(
'templates/doc-pandoc.Rmd', package = 'liftr')), collapse = '\n')
}
}
}
parse_cran = function(cran) {
if (!is.null(cran)) {
liftr_cran = quote_str(cran)
tmp = tempfile()
invisible(knit(
input = system.file(
'templates/pkg-cran.Rmd', package = 'liftr'),
output = tmp, quiet = TRUE))
liftr_cran = readLines(tmp)
liftr_cran
} else {
NULL
}
}
parse_bioc = function(bioc) {
if (!is.null(bioc)) {
liftr_bioc = quote_str(bioc)
tmp = tempfile()
invisible(knit(
input = system.file(
'templates/pkg-bioc.Rmd', package = 'liftr'),
output = tmp, quiet = TRUE))
liftr_bioc = readLines(tmp)
liftr_bioc
} else {
NULL
}
}
parse_remotes = function(remotes) {
if (!is.null(remotes)) {
liftr_remotes = quote_str(remotes)
tmp = tempfile()
invisible(knit(
input = system.file(
'templates/pkg-remotes.Rmd', package = 'liftr'),
output = tmp, quiet = TRUE))
liftr_remotes = readLines(tmp)
liftr_remotes
} else {
NULL
}
}
parse_include = function(input, include) {
if (!is.null(include)) {
include_file_path = normalizePath(
paste0(file_dir(input), '/', include))
if (!file.exists(include_file_path))
stop('include file does not exist')
paste(readLines(include_file_path), collapse = '\n')
} else {
NULL
}
}
#' Purge Docker Container
#'
#' This function stops and removes the Docker container used
#' for rendering the R Markdown document, by running
#' \code{docker stop} and \code{docker rm} commands.
#'
#' @param input_yml The YAML file (default output of
#' \code{\link{render_docker}}) storing the information
#' of the container to be stopped and removed.
#'
#' @importFrom yaml yaml.load_file
#'
#' @export purge_container
#'
#' @return purge results
#'
#' @examples
#' \dontrun{
#' purge_container("liftr-minimal.docker.yml")}
purge_container = function(input_yml) {
if (is.null(input_yml))
stop('missing input file')
if (!file.exists(normalizePath(input_yml)))
stop('input file does not exist')
lst = yaml.load_file(normalizePath(input_yml))
container_name = lst$'container_name'
# TODO: needs exception handling
system(paste0("docker stop \"", container_name, "\""))
system(paste0("docker rm -f \"", container_name, "\""))
}
#' Purge Docker Image
#'
#' This function removes the Docker image used
#' for rendering the R Markdown document by
#' running \code{docker rmi} commands.
#'
#' @param input_yml The YAML file (default output of
#' \code{\link{render_docker}}) storing the information
#' of the image to be removed.
#'
#' @importFrom yaml yaml.load_file
#'
#' @export purge_image
#'
#' @return status
#'
#' @examples
#' \dontrun{
#' purge_image("liftr-minimal.docker.yml")}
purge_image = function(input_yml) {
if (is.null(input_yml))
stop('missing input file')
if (!file.exists(normalizePath(input_yml)))
stop('input file does not exist')
lst = yaml.load_file(normalizePath(input_yml))
image_name = lst$'image_name'
# TODO: needs exception handling
system(paste0("docker rmi -f \"", image_name, "\""))
}
#' Render Dockerized R Markdown Documents
#'
#' @description
#' Render dockerized R Markdown documents using Docker containers.
#'
#' @details
#' Before using this function, please run \code{\link{lift}} on the
#' RMD document first to generate the \code{Dockerfile}.
#'
#' After a successful rendering, you will be able to clean up the
#' Docker image with \code{\link{purge_image}}.
#'
#' Please see \code{vignette('liftr-intro')} for details of the extended
#' YAML metadata format and system requirements for rendering dockerized
#' R Markdown documents.
#'
#' @param input Input file to render in Docker container.
#' @param tag Docker image name to build, sent as docker argument \code{-t}.
#' If not specified, it will use the same name as the input file.
#' @param container_name Docker container name to run.
#' If not specified, will use a randomly generated name.
#' @param cache Logical. Controls the \code{--no-cache} argument
#' in \code{docker run}. Setting this to be \code{TRUE} can accelerate
#' the rendering speed substantially for repeated/interactive rendering
#' since the Docker image layers will be cached, with only the changed
#' (knitr related) image layer being updated. Default is \code{TRUE}.
#' @param purge_info Logical. Should we write the Docker container and
#' image information to a YAML file for purging later?
#' Default is \code{TRUE}.
#' @param build_args A character string specifying additional
#' \code{docker build} arguments. For example,
#' \code{--pull=true -m="1024m" --memory-swap="-1"}.
#' @param run_args A character string specifying additional
#' \code{docker run} arguments. For example, \code{--privileged=true}.
#' @param ... Additional arguments passed to
#' \code{\link[rmarkdown]{render}}.
#'
#' @return
#' \itemize{
#' \item A list containing the image name, container name,
#' and Docker commands will be returned.
#' \item An YAML file ending with \code{.docker.yml} storing the
#' image name, container name, and Docker commands for rendering
#' this document will be written to the directory of the input file.
#' \item The rendered output will be written to the directory of the
#' input file.
#' }
#'
#' @export render_docker
#'
#' @importFrom rmarkdown render
#' @importFrom yaml as.yaml
#'
#' @examples
## Included in \dontrun{} since users need Docker installed to run them.
#' # copy example file
#' dir_example = paste0(tempdir(), "/liftr-tidyverse/")
#' dir.create(dir_example)
#' file.copy(system.file("examples/liftr-tidyverse.Rmd", package = "liftr"), dir_example)
#'
#' # containerization
#' input = paste0(dir_example, "liftr-tidyverse.Rmd")
#' lift(input)
#'
#' \dontrun{
#' # render the document with Docker
#' render_docker(input)
#'
#' # view rendered document
#' browseURL(paste0(dir_example, "liftr-tidyverse.pdf"))
#'
#' # purge the generated Docker image
#' purge_image(paste0(dir_example, "liftr-tidyverse.docker.yml"))}
render_docker = function(
input = NULL, tag = NULL, container_name = NULL,
cache = TRUE, purge_info = TRUE,
build_args = NULL, run_args = NULL, ...) {
if (is.null(input))
stop('missing input file')
if (!file.exists(normalizePath(input)))
stop('input file does not exist')
# docker build
dockerfile_path = paste0(file_dir(input), '/Dockerfile')
if (!file.exists(dockerfile_path))
stop('Cannot find Dockerfile in the same directory of input file,
please dockerize the R Markdown document via lift() first.')
if (Sys.which('docker') == '')
stop('Cannot find `docker` on system search path,
please ensure we can use `docker` from shell')
image_name = ifelse(is.null(tag), file_name_sans(input), tag)
cache = paste0("--no-cache=", ifelse(cache, "false", "true"))
docker_build_cmd = paste0(
"docker build ", cache, " --rm=true ",
build_args, " -t=\"", image_name, "\" ",
file_dir(dockerfile_path))
# docker run
container_name = ifelse(
is.null(container_name),
paste0('liftr_container_', uuid()),
container_name)
docker_run_cmd_base = paste0(
"docker run --rm ", run_args,
" --name \"", container_name,
"\" -u `id -u $USER` -v \"",
file_dir(dockerfile_path), ":", "/liftrroot/\" ",
image_name,
" Rscript -e \"library('knitr');library('rmarkdown');",
"library('shiny');setwd('/liftrroot/');")
# process additional arguments passed to rmarkdown::render()
dots_arg = list(...)
if (length(dots_arg) == 0L) {
docker_run_cmd = paste0(
docker_run_cmd_base, "render(input = '",
file_name(input), "')\"")
} else {
if (!is.null(dots_arg$input))
stop('input can only be specified once')
if (!is.null(dots_arg$output_file) |
!is.null(dots_arg$output_dir) |
!is.null(dots_arg$intermediates_dir)) {
stop('`output_file`, `output_dir`, and `intermediates_dir`
are not supported to be changed now, we will consider
this in the next versions.')
}
dots_arg$input = file_name(input)
tmp = tempfile()
dput(dots_arg, file = tmp)
render_args = paste0(readLines(tmp), collapse = '\n')
render_cmd = paste0("do.call(render, ", render_args, ')')
docker_run_cmd = paste0(docker_run_cmd_base, render_cmd, "\"")
}
# output container and image info before rendering
res = list(
'container_name' = container_name,
'image_name' = image_name,
'docker_build_cmd' = docker_build_cmd,
'docker_run_cmd' = docker_run_cmd)
if (purge_info) {
writeLines(as.yaml(res), con = paste0(
file_dir(input), '/', file_name_sans(input), '.docker.yml'))
}
# render
system(docker_build_cmd)
system(docker_run_cmd)
res
}
#' @rdname render_docker
#' @export drender
drender = function(...) {
.Deprecated('render_docker')
}
# convert string vectors to single quote-marked
# string separated by comma
quote_str = function(x) {
paste0("\'", paste(x, collapse = "','"), "\'")
}
# get directory of the file
file_dir = function(x) {
dirname(normalizePath(x))
}
# get file name with extension
file_name = function(x) {
basename(normalizePath(x))
}
# get file name without extension
sans_ext = tools::file_path_sans_ext
file_name_sans = function(x) {
basename(sans_ext(normalizePath(x)))
}
# generate UUID for Docker container names
# derived from https://stackoverflow.com/questions/10492817/
uuid = function() {
id = paste(sample(c(letters[1:6], 0:9), 30, replace = TRUE), collapse = '')
paste(
substr(id, 1, 8), '_', substr(id, 9, 12), '_', '4',
substr(id, 13, 15), '_', sample(c('8', '9', 'a', 'b'), 1),
substr(id, 16, 18), '_', substr(id, 19, 30), sep = '',
collapse = '')
}
#' check if from Bioconductor base images
#' @importFrom stringr str_trim
#' @noRd
is_from_bioc = function(x) {
substr(str_trim(x), 1L, 13L) == 'bioconductor/'
}
#' check if from the rocker/rstudio base image
#' @importFrom stringr str_trim
#' @noRd
is_from_rstudio = function(x) {
substr(str_trim(x), 1L, 14L) == 'rocker/rstudio'
}
# remove consecutive blank lines and only keep one
sanitize_blank = function(txt) {
blank = which(txt == "")
# if no blank lines at all
if (length(blank) == 0L) return(txt)
idx = which(diff(blank) == 1L) + 1L
# if no consecutive blank lines
if (length(idx) == 0L) return(