lift.R 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. #' Dockerize R Markdown Documents
  2. #'
  3. #' Generate \code{Dockerfile} for R Markdown documents.
  4. #' Rabix is supported if there is certain metadata in the R Markdown
  5. #' document: the function will generate a \code{Rabixfile} containing
  6. #' the parsed running parameters under the output directory.
  7. #'
  8. #' After running \link{lift}, run \link{drender} on the document to
  9. #' render the Dockerized R Markdown document using Docker containers.
  10. #' See \code{vignette('liftr-intro')} for details about the extended
  11. #' YAML front-matter metadata format used by liftr.
  12. #'
  13. #' @param input Input (R Markdown or Shiny R markdown) file or shiny app folder.
  14. #' @param output_dir Directory to output \code{Dockerfile}.
  15. #' If not provided, will be the same directory as \code{input}.
  16. #' @param dockerfile a Dockerfile path, if not, use our template.
  17. #' @param ... Extra arguments passed to liftShinyApp function.
  18. #'
  19. #' @return \code{Dockerfile} (and \code{Rabixfile} if possible).
  20. #'
  21. #' @export lift
  22. #'
  23. #' @importFrom knitr knit
  24. #' @importFrom yaml yaml.load as.yaml
  25. #' @importFrom rsconnect appDependencies
  26. #'
  27. #' @examples
  28. #' # 1. Dockerized R Markdown document
  29. #' dir_docker = paste0(tempdir(), '/lift_docker/')
  30. #' dir.create(dir_docker)
  31. #' file.copy(system.file("examples/docker.Rmd", package = "liftr"), dir_docker)
  32. #' # use lift() to parse Rmd and generate Dockerfile
  33. #' lift(paste0(dir_docker, "docker.Rmd"))
  34. #' # view generated Dockerfile
  35. #' readLines(paste0(dir_docker, "Dockerfile"))
  36. #'
  37. #' # 2. Dockerized R Markdown document with Rabix options
  38. #' dir_rabix = paste0(tempdir(), '/lift_rabix/')
  39. #' dir.create(dir_rabix)
  40. #' file.copy(system.file("template/rabix.Rmd", package = "liftr"), dir_rabix)
  41. #' lift(input = paste0(dir_rabix, "rabix.Rmd"))
  42. #' # view generated Dockerfile
  43. #' readLines(paste0(dir_rabix, "Dockerfile"))
  44. #' # view generated Rabixfile
  45. #' readLines(paste0(dir_rabix, "Rabixfile"))
  46. lift = function(input = NULL, output_dir = NULL, dockerfile = NULL, ...) {
  47. if (is.null(input))
  48. stop('missing input file')
  49. if(!is.na(file.info(input)$isdir) && file.info(input)$isdir){
  50. if(is_shinyapp(input)){
  51. message("parsing shiny app dependecies and genrate liftr file ...")
  52. lift_shinyapp(input, output_dir = output_dir, ...)
  53. return()
  54. }else{
  55. stop("your input folder is not a shinyapp folder")
  56. }
  57. }else{
  58. ## treat as file
  59. if (!file.exists(input)){
  60. stop('input file or shiny app folder does not exist')
  61. }
  62. }
  63. opt_all_list = parse_rmd(input)
  64. # liftr options handling
  65. if (is.null(opt_all_list$liftr))
  66. stop('Cannot find `liftr` option in file header')
  67. opt_list = opt_all_list$liftr
  68. # base image
  69. if (!is.null(opt_list$from)) {
  70. liftr_from = opt_list$from
  71. } else {
  72. liftr_from = 'rocker/r-base:latest'
  73. }
  74. # maintainer name
  75. if (!is.null(opt_list$maintainer)) {
  76. liftr_maintainer = opt_list$maintainer
  77. } else {
  78. stop('Cannot find `maintainer` option in file header')
  79. }
  80. if (!is.null(opt_list$maintainer_email)) {
  81. liftr_maintainer_email = opt_list$maintainer_email
  82. } else {
  83. stop('Cannot find `maintainer_email` option in file header')
  84. }
  85. # system dependencies
  86. if (!is.null(opt_list$syslib)) {
  87. liftr_syslib =
  88. paste(readLines(system.file('template/syslib.Rmd', package = 'liftr')),
  89. paste(opt_list$syslib, collapse = ' '), sep = ' ')
  90. } else {
  91. liftr_syslib = NULL
  92. }
  93. # texlive
  94. if (!is.null(opt_list$latex)) {
  95. if (opt_list$latex == TRUE) {
  96. liftr_texlive =
  97. paste(readLines(system.file('template/texlive.Rmd', package = 'liftr')),
  98. collapse = '\n')
  99. } else {
  100. liftr_texlive = NULL
  101. }
  102. } else {
  103. liftr_texlive = NULL
  104. }
  105. # pandoc
  106. # this solves https://github.com/road2stat/liftr/issues/12
  107. if (is_from_bioc(liftr_from) | is_from_rstudio(liftr_from)) {
  108. liftr_pandoc = NULL
  109. } else {
  110. if (!is.null(opt_list$pandoc)) {
  111. if (opt_list$pandoc == FALSE) {
  112. liftr_pandoc = NULL
  113. } else {
  114. liftr_pandoc = paste(readLines(
  115. system.file('template/pandoc.Rmd', package = 'liftr')), collapse = '\n')
  116. }
  117. } else {
  118. liftr_pandoc = NULL
  119. ## liftr_pandoc = paste(readLines(
  120. ## system.file('template/pandoc.Rmd', package = 'liftr')), collapse = '\n')
  121. }
  122. }
  123. # Factory packages
  124. liftr_factorypkgs = c('devtools', 'knitr', 'rmarkdown', 'shiny', 'RCurl', 'rsconnect')
  125. liftr_factorypkg = quote_str(liftr_factorypkgs)
  126. # CRAN packages
  127. if (!is.null(opt_list$cranpkg)) {
  128. liftr_cranpkgs = quote_str(opt_list$cranpkg)
  129. tmp = tempfile()
  130. invisible(knit(input = system.file('template/cranpkg.Rmd', package = 'liftr'),
  131. output = tmp, quiet = TRUE))
  132. liftr_cranpkg = readLines(tmp)
  133. } else {
  134. liftr_cranpkg = NULL
  135. }
  136. # Bioconductor packages
  137. if (!is.null(opt_list$biocpkg)) {
  138. liftr_biocpkgs = quote_str(opt_list$biocpkg)
  139. tmp = tempfile()
  140. invisible(knit(input = system.file('template/biocpkg.Rmd', package = 'liftr'),
  141. output = tmp, quiet = TRUE))
  142. liftr_biocpkg = readLines(tmp)
  143. } else {
  144. liftr_biocpkg = NULL
  145. }
  146. # GitHub packages
  147. if (!is.null(opt_list$ghpkg)) {
  148. liftr_ghpkgs = quote_str(opt_list$ghpkg)
  149. tmp = tempfile()
  150. invisible(knit(input = system.file('template/ghpkg.Rmd', package = 'liftr'),
  151. output = tmp,
  152. quiet = TRUE))
  153. liftr_ghpkg = readLines(tmp)
  154. } else {
  155. liftr_ghpkg = NULL
  156. }
  157. # extra: plain docker file line, like ADD, COPY, CMD etc
  158. if (!is.null(opt_list$extra)) {
  159. liftr_extra = opt_list$extra
  160. if(get_type(input) == "shinydoc"){
  161. liftr_extra = c(liftr_extra,
  162. paste0("ADD ", basename(input), " /srv/shiny-server/"))
  163. }
  164. } else {
  165. if(get_type(input) == "shinydoc"){
  166. liftr_extra = paste0("ADD ", basename(input), " /srv/shiny-server/")
  167. }else{
  168. liftr_extra = NULL
  169. }
  170. }
  171. # write Dockerfile
  172. if (is.null(output_dir)) output_dir = file_dir(input)
  173. if(is.null(dockerfile)){
  174. dockerfile = system.file('template/Dockerfile.Rmd', package = 'liftr')
  175. }
  176. .out.dockerfile = paste0(normalizePath(output_dir), '/Dockerfile')
  177. message("Dockerfile:", .out.dockerfile)
  178. invisible(knit(dockerfile,
  179. output = .out.dockerfile,
  180. quiet = TRUE))
  181. # handling rabix info
  182. if (!is.null(opt_list$rabix)) {
  183. if (opt_list$rabix == TRUE) {
  184. if (is.null(opt_list$rabix_d))
  185. stop('Cannot find `rabix_d` option in file header')
  186. liftr_rabix_d = paste0('\"', normalizePath(opt_list$rabix_d,
  187. mustWork = FALSE), '\"')
  188. if (is.null(opt_list$rabix_json))
  189. stop('Cannot find `rabix_json` option in file header')
  190. liftr_rabix_json = paste0('\"', opt_list$rabix_json, '\"')
  191. if (!is.null(opt_list$rabix_args)) {
  192. liftr_rabix_with_args = '-- '
  193. rabix_args_vec = unlist(opt_list$rabix_args)
  194. liftr_rabix_args =
  195. paste(paste0('--', paste(names(rabix_args_vec),
  196. rabix_args_vec)),
  197. collapse = ' ')
  198. } else {
  199. liftr_rabix_with_args = NULL
  200. liftr_rabix_args = NULL
  201. }
  202. .out.rabixfile = paste0(normalizePath(output_dir), '/Rabixfile')
  203. invisible(knit(system.file('template/Rabixfile.Rmd',
  204. package = 'liftr'),
  205. output = .out.rabixfile,
  206. quiet = TRUE))
  207. }
  208. }
  209. return(list(dockerfile = .out.dockerfile, rmd = input))
  210. }
  211. #' parse Rmarkdown header
  212. #'
  213. #' parse Rmarkdown header and return a list
  214. #'
  215. #' The header section is use three hyphens --- as start line & end line,
  216. #' or three hyphens --- as start line with three dots ...
  217. #' as end line
  218. #'
  219. #' @param input Rmd file to be parsed into a list
  220. #'
  221. #' @export parse_rmd
  222. #' @aliases parse_rmd
  223. #' @examples
  224. #' fl = system.file("examples/docker.Rmd", package = "liftr")
  225. #' parse_rmd(fl)
  226. parse_rmd = function(input){
  227. # locate YAML metadata block
  228. doc_content = readLines(normalizePath(input))
  229. header_pos = which(doc_content == '---')
  230. # handling YAML blocks ending with three dots
  231. if (length(header_pos) == 1L) {
  232. header_dot_pos = which(doc_content == '...')
  233. if (length(header_dot_pos) == 0L) {
  234. stop('Cannot correctly locate YAML metadata block.
  235. Please use three hyphens (---) as start line & end line,
  236. or three hyphens (---) as start line with three dots (...)
  237. as end line.')
  238. } else {
  239. header_pos[2L] = header_dot_pos[1L]
  240. }
  241. }
  242. doc_yaml = paste(doc_content[(header_pos[1L] + 1L):
  243. (header_pos[2L] - 1L)],
  244. collapse = '\n')
  245. yaml.load(doc_yaml)
  246. }
  247. trans_name <- function(x){
  248. names(x)[names(x) == "Bioconductor"] <- "biocpkg"
  249. names(x)[names(x) == "CRAN"] <- "cranpkg"
  250. x
  251. }
  252. create_lift_file = function(appDir = getwd(), appFiles = NULL, output_file = "docker.Rmd",
  253. maintainer = NULL, email = NULL,
  254. from = "rocker/shiny"){
  255. stopifnot(dir.exists(appDir))
  256. .out <- file.path(normalizePath(dirname(appDir)), output_file)
  257. .base <- paste0("COPY ", basename(appDir), " /srv/shiny-server/", basename(appDir))
  258. ## add dummy maintain name
  259. if(is.null(maintainer)){
  260. maintainer = Sys.info()[names(Sys.info()) == "user"]
  261. message("maintainer name is not provided, user your system user name as maintainer name: ", maintainer)
  262. }
  263. if(is.null(email)){
  264. email = paste0(maintainer, "@dummy.com")
  265. message("email is not provided, create fake email address for placeholder: ", email)
  266. }
  267. .h <- list(maintainer = maintainer,
  268. maintainer_email = email,
  269. from = from,
  270. extra = list(.base),
  271. shiny = TRUE)
  272. ## add dummy email name
  273. ## search for liftr.rmd
  274. if(!file.exists(.out)){
  275. ad = appDependencies(appDir = appDir, appFiles = appFiles)
  276. lst = by(ad, ad$source, function(x){
  277. as.list(x$package)
  278. })
  279. lst = trans_name(lst)
  280. res = list(liftr = c(.h,lst))
  281. message("Shiny liftr file: ", .out)
  282. con = file(.out)
  283. txt = "---"
  284. txt = c(txt, as.yaml(res))
  285. txt = c(txt, "---")
  286. writeLines(txt, con = .out)
  287. close(con)
  288. }else{
  289. message(.out, " exists.")
  290. }
  291. .out
  292. }
  293. #' Dockerize an Shiny App
  294. #'
  295. #' Parse dependecies from a shiny app folder and lift it into a Dockerfile
  296. #'
  297. #' @param appDir Directory containing application. Defaults to current working directory.
  298. #' @param appFiles The files and directories to bundle and deploy (only if upload = TRUE). Can be NULL, in which case all the files in the directory containing the application are bundled. Takes precedence over appFileManifest if both are supplied.
  299. #' @param output_file A temporariy R markdown file with liftr header passed from shina app folder.
  300. #' @param output_dir output_dir Directory to output \code{Dockerfile}. If not provided, will be the same directory as \code{input}.
  301. #' @param maintainer maintainer information for Dockerfile
  302. #' @param email email address for Dockerfile
  303. #' @param shiny_base base image for shiny, by default it's rocker/shiny
  304. #' @export lift_shinyapp
  305. #' @aliases lift_shinyapp
  306. #' @examples
  307. #' \dontrun{
  308. #' lift_shinayapp("test_app_folder")
  309. #' }
  310. lift_shinyapp <- function(appDir = getwd(), appFiles = NULL, output_file = "docker.Rmd", output_dir = NULL,
  311. maintainer = NULL, email = NULL,
  312. shiny_base = "rocker/shiny"){
  313. .out <- create_lift_file(appDir = appDir, appFiles = appFiles, output_file = output_file,
  314. maintainer = maintainer, email = email,
  315. from = shiny_base)
  316. lift(.out, output_dir)
  317. }
  318. #' lift a docopt string
  319. #'
  320. #' lift a docopt string used for command line
  321. #'
  322. #' parse Rmarkdown header from rabix field
  323. #'
  324. #' @param input input Rmarkdown file or a function name (character)
  325. #' @export lift_docopt
  326. #' @aliases lift_docopt
  327. #' @return a string used for docopt
  328. #' @examples
  329. #' fl = system.file("examples/runif.Rmd", package = "liftr")
  330. #' opts = lift_docopt(fl)
  331. #' \dontrun{
  332. #' require(docopt)
  333. #' docopt(opts)
  334. #' docopt(lift_docopt("mean.default"))
  335. #' }
  336. lift_docopt = function(input){
  337. if(file.exists(input)){
  338. res = lift_docopt_from_header(input)
  339. }else{
  340. message("file doesn't exist, try to try this as a function")
  341. res = lift_docopt_from_function(input)
  342. }
  343. res
  344. }
  345. lift_docopt_from_header = function(input){
  346. opt_all_list = parse_rmd(input)
  347. ol <- opt_all_list$rabix
  348. .in <- ol$inputs
  349. txt <- paste("usage:", ol$baseCommand, "[options]")
  350. txt <- c(txt, "options:")
  351. ol <- lapply(.in, function(x){
  352. .nm <- x$prefix
  353. .t <- x$type
  354. .type <- paste0('<', deType(.t), '>')
  355. .o <- paste(.nm, .type, sep = "=")
  356. .des <- x$description
  357. .default <- x$default
  358. if(!is.null(.default)){
  359. .des <- paste0(.des, " [default: ", .default, "]")
  360. }
  361. list(name = .o, description = .des)
  362. })
  363. for(i in 1:length(ol)){
  364. txt <- c(txt, paste(" ", ol[[i]]$name, ol[[i]]$description))
  365. }
  366. paste(txt, collapse = "\n")
  367. }
  368. lift_docopt_from_function = function(input){
  369. ol = opt_all_list = rdarg(input)
  370. txt <- paste0("usage: ", input, ".R", " [options]")
  371. nms <- names(ol)
  372. lst <- NULL
  373. for(nm in nms){
  374. .nm = paste0("--", nm)
  375. .t = guess_type(nm, input)
  376. .type = paste0('<', deType(.t), '>')
  377. .o = paste(.nm, .type, sep = "=")
  378. .des = ol[[nm]]
  379. .def = guess_default(nm, input)
  380. if(!is.null(.def)){
  381. .des <- paste0(.des, " [default: ", .def, "]")
  382. }
  383. lst = c(lst, list(list(name = .o, description = .des)))
  384. }
  385. for(i in 1:length(lst)){
  386. txt <- c(txt, paste(" ", lst[[i]]$name, lst[[i]]$description))
  387. }
  388. ## Fixme:
  389. paste(txt, collapse = "\n")
  390. }
  391. lift_cmd = function(input, output_dir = NULL, shebang = "#!/usr/local/bin/Rscript",
  392. docker_root = "/"){
  393. if(file.exists(input)){
  394. opt_all_list = parse_rmd(input)
  395. if (is.null(output_dir))
  396. output_dir = dirname(normalizePath(input))
  397. tmp = file.path(output_dir, opt_all_list$rabix$baseCommand)
  398. message("command line file: ", tmp)
  399. con = file(tmp)
  400. txt = lift_docopt(input)
  401. txt = c(shebang, "'", paste0(txt, " ' -> doc"))
  402. paste("library(docopt)\n opts <- docopt(doc) \n
  403. rmarkdown::render('",
  404. docker_root, basename(input), "', BiocStyle::html_document(toc = TRUE),
  405. output_dir = '.', params = lst)
  406. " )-> .final
  407. txt <- c(txt, .final)
  408. writeLines(txt, con = con)
  409. close(con)
  410. }else{
  411. message("consider you passed a function name (character)")
  412. if (is.null(output_dir))
  413. output_dir = getwd()
  414. .baseCommand <- paste0(input, ".R")
  415. tmp = file.path(output_dir, .baseCommand)
  416. message("command line file: ", tmp)
  417. con = file(tmp)
  418. txt = lift_docopt(input)
  419. txt = c(shebang, "'", paste0(txt, " ' -> doc"))
  420. txt = c(txt, "library(docopt)\n opts <- docopt(doc)")
  421. .final = gen_list(input)
  422. txt <- c(txt, .final)
  423. writeLines(txt, con = con)
  424. close(con)
  425. }
  426. Sys.chmod(tmp)
  427. tmp
  428. }
  429. con_fun = function(type){
  430. res = switch(deType(type),
  431. int = "as.integer",
  432. float = "as.numeric",
  433. boolean = "as.logical",
  434. NULL)
  435. res
  436. }
  437. gen_list = function(fun){
  438. lst = rdarg(fun)
  439. lst = lst[names(lst) != "..."]
  440. nms = names(lst)
  441. txt = NULL
  442. for(nm in nms){
  443. .t = con_fun(guess_type(nm, fun))
  444. if(!is.null(.t)){
  445. txt = c(txt, paste0(nm, " = ", .t, "(", "opts$", nm, ")"))
  446. }else{
  447. txt = c(txt, paste0(nm, " = ", "opts$", nm))
  448. }
  449. }
  450. txt = paste("list(", paste(txt, collapse = ","), ")")
  451. paste("do.call(", fun, ",", txt, ")")
  452. }
  453. guess_type = function(nm, fun){
  454. dl = formals(fun)
  455. if(!is.null(dl[[nm]])){
  456. .c <- class(dl[[nm]])
  457. if(.c == "name"){
  458. return("string")
  459. }else{
  460. return(deType(.c))
  461. }
  462. }else{
  463. return("string")
  464. }
  465. }
  466. guess_default = function(nm, fun){
  467. dl = formals(fun)
  468. if(!is.null(dl[[nm]])){
  469. .c <- class(dl[[nm]])
  470. if(.c == "name"){
  471. return(NULL)
  472. }else{
  473. return(dl[[nm]])
  474. }
  475. }else{
  476. return(NULL)
  477. }
  478. }
  479. #' #' install from liftr rmarkdown with headers
  480. #' #'
  481. #' #' install from liftr rmarkdown with headers
  482. #' #' @param rmd A rmarkdown with lift header
  483. #' #' @export install_from_rmd
  484. #' #' @examples
  485. #' #' \dontrun{
  486. #' #' install_from_rmd("test.rmd")
  487. #' #' }
  488. #' install_from_rmd = function(rmd){
  489. #' opt_list = parse_rmd(rmd)
  490. #' liftr_cranpkgs = opt_list$liftr$cranpkg
  491. #' liftr_biocpkgs = opt_list$liftr$biocpkg
  492. #' liftr_ghpkgs = opt_list$liftr$ghpkg
  493. #' if(!is.null(liftr_cranpkgs)){
  494. #' source('https://cdn.rawgit.com/road2stat/liftrlib/fab41764ea8b56677d05c70c86225774164b6ca0/install_cran.R')
  495. #' install_cran(liftr_cranpkgs)
  496. #' }
  497. #' if(!is.null(liftr_biocpkgs)){
  498. #' source('http://bioconductor.org/biocLite.R')
  499. #' biocLite(c(liftr_biocpkgs))
  500. #' }
  501. #' if(!is.null(liftr_ghpkgs)){
  502. #' devtools::install_github(c(liftr_ghpkgs))
  503. #' }
  504. #'
  505. #' }
  506. #'