Skip to content

Commit 427724f

Browse files
schloerkeyihui
authored andcommitted
Force a render when user pkgs don't match the pkgs used in a rendered, shiny-prerendered document (#1420)
Fixes rstudio/learnr#169
1 parent 6931cdb commit 427724f

File tree

4 files changed

+152
-53
lines changed

4 files changed

+152
-53
lines changed

NEWS.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ rmarkdown 1.11 (unreleased)
1717

1818
* Fixed the website navbar not being able to display submenus properly (#721, #1426).
1919

20+
* Added checks for shiny-prerendered documents to find all html dependencies, match all execution packages, and match the major R version (#1420).
21+
22+
2023
rmarkdown 1.10
2124
================================================================================
2225

@@ -549,4 +552,3 @@ rmarkdown 0.3.11
549552
================================================================================
550553

551554
Initial release to CRAN
552-

R/render.R

+4-1
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,10 @@ render <- function(input,
360360
# force various output options
361361
output_options$self_contained <- FALSE
362362
output_options$dependency_resolver <- function(deps) {
363-
shiny_prerendered_dependencies <<- deps
363+
shiny_prerendered_dependencies <<- list(
364+
deps = deps,
365+
packages = get_loaded_packages()
366+
)
364367
list()
365368
}
366369
}

R/shiny_prerendered.R

+121-51
Original file line numberDiff line numberDiff line change
@@ -75,49 +75,13 @@ shiny_prerendered_html <- function(input_rmd, encoding, render_args) {
7575

7676
# determine whether we need to render the Rmd in advance
7777
prerender_option <- tolower(Sys.getenv("RMARKDOWN_RUN_PRERENDER", "1"))
78-
79-
if (file.access(output_dir, 2) != 0) {
80-
if (!file.exists(rendered_html))
81-
stop("Unable to write prerendered HTML file to ", rendered_html)
82-
83-
prerender <- FALSE
84-
}
85-
else if (identical(prerender_option, "0")) {
86-
prerender <- FALSE
87-
}
88-
else if (identical(prerender_option, "1")) {
89-
90-
# determine the last modified time of the output file
91-
if (file.exists(rendered_html))
92-
output_last_modified <- as.integer(file.info(rendered_html)$mtime)
93-
else
94-
output_last_modified <- 0L
95-
96-
# short circuit for Rmd modified. if it hasn't been modified since the
97-
# html was generated look at external resources
98-
input_last_modified <- as.integer(file.info(input_rmd)$mtime)
99-
if (input_last_modified > output_last_modified) {
100-
prerender <- TRUE
101-
}
102-
else {
103-
# find external resources referenced by the file
104-
external_resources <- find_external_resources(input_rmd, encoding)
105-
106-
# get paths to external resources
107-
input_files <- c(input_rmd,
108-
file.path(output_dir, external_resources$path))
109-
110-
# what's the maximum last_modified time of an input file
111-
input_last_modified <- max(as.integer(file.info(input_files)$mtime),
112-
na.rm = TRUE)
113-
114-
# render if an input file was modified after the output file
115-
prerender <- input_last_modified > output_last_modified
116-
}
117-
}
118-
else {
119-
stop("Invalid value '", prerender_option, "' for RMARKDOWN_RUN_PRERENDER")
120-
}
78+
prerender <- shiny_prerendered_prerender(
79+
input_rmd,
80+
rendered_html,
81+
output_dir,
82+
encoding,
83+
prerender_option
84+
)
12185

12286
# prerender if necessary
12387
if (prerender) {
@@ -171,17 +135,114 @@ shiny_prerendered_html <- function(input_rmd, encoding, render_args) {
171135
shinyHTML_with_deps(rendered_html, dependencies)
172136
}
173137

138+
shiny_prerendered_prerender <- function(
139+
input_rmd,
140+
rendered_html,
141+
output_dir,
142+
encoding,
143+
prerender_option
144+
) {
145+
if (file.access(output_dir, 2) != 0) {
146+
if (!file.exists(rendered_html))
147+
stop("Unable to write prerendered HTML file to ", rendered_html)
148+
return(FALSE)
149+
}
150+
151+
if (identical(prerender_option, "0")) {
152+
return(FALSE)
153+
}
154+
if (!identical(prerender_option, "1")) {
155+
stop("Invalid value '", prerender_option, "' for RMARKDOWN_RUN_PRERENDER")
156+
}
157+
158+
# determine the last modified time of the output file
159+
if (file.exists(rendered_html)) {
160+
output_last_modified <- as.integer(file.info(rendered_html)$mtime)
161+
} else {
162+
output_last_modified <- 0L
163+
}
164+
165+
# short circuit for Rmd modified. if it hasn't been modified since the
166+
# html was generated look at external resources
167+
input_last_modified <- as.integer(file.info(input_rmd)$mtime)
168+
if (input_last_modified > output_last_modified) {
169+
return(TRUE)
170+
}
171+
172+
# find external resources referenced by the file
173+
external_resources <- find_external_resources(input_rmd, encoding)
174+
175+
# get paths to external resources
176+
input_files <- c(input_rmd, file.path(output_dir, external_resources$path))
177+
178+
# what's the maximum last_modified time of an input file
179+
input_last_modified <- max(as.integer(file.info(input_files)$mtime), na.rm = TRUE)
180+
181+
# render if an input file was modified after the output file
182+
if (input_last_modified > output_last_modified) {
183+
return(TRUE)
184+
}
185+
186+
html_lines <- readLines(rendered_html, encoding = "UTF-8", warn = FALSE)
187+
188+
# check that all html dependencies exist
189+
dependencies_json <- shiny_prerendered_extract_context(html_lines, "dependencies")
190+
dependencies <- jsonlite::unserializeJSON(dependencies_json)
191+
192+
pkgsSeen <- list()
193+
for (dep in dependencies) {
194+
if (is.null(dep$package)) {
195+
# if the file doesn't exist at all, render again
196+
if (!file.exists(dep$src$file)) {
197+
# might create a missing file compile-time error,
198+
# but that's better than a missing file prerendered error
199+
return(TRUE)
200+
}
201+
} else {
202+
depPkg <- dep$package
203+
depVer <- dep$pkgVersion
204+
if (is.null(pkgsSeen[[depPkg]])) {
205+
# has not seen pkg
206+
207+
# depVer could be NULL, producing a logical(0)
208+
# means old prerender version, render again
209+
if (!isTRUE(get_package_version_string(depPkg) == depVer)) {
210+
# was not rendered with the same R package. must render again
211+
return (TRUE)
212+
}
213+
pkgsSeen[[depPkg]] <- depVer
214+
}
215+
}
216+
}
217+
# all html dependencies are accounted for
218+
219+
# check for execution package version differences
220+
execution_json <- shiny_prerendered_extract_context(html_lines, "execution_dependencies")
221+
execution_info <- jsonlite::unserializeJSON(execution_json)
222+
execution_pkg_names <- execution_info$packages$package
223+
execution_pkg_versions <- execution_info$packages$version
224+
for (i in seq_along(execution_pkg_names)) {
225+
if (!identical(
226+
get_package_version_string(execution_pkg_names[i]),
227+
execution_pkg_versions[i]
228+
)) {
229+
return(TRUE)
230+
}
231+
}
232+
# all execution packages match
233+
234+
return(FALSE)
235+
}
236+
174237

175238
# Write the dependencies for a shiny_prerendered document.
176239
shiny_prerendered_append_dependencies <- function(input, # always UTF-8
177240
shiny_prerendered_dependencies,
178241
files_dir,
179242
output_dir) {
180243

181-
182-
183244
# transform dependencies (if we aren't in debug mode)
184-
dependencies <- lapply(shiny_prerendered_dependencies, function(dependency) {
245+
dependencies <- lapply(shiny_prerendered_dependencies$deps, function(dependency) {
185246

186247
# no transformation in dev mode (so browser dev tools can map directly
187248
# to the locations of CSS and JS files in their pkg src directory)
@@ -199,6 +260,8 @@ shiny_prerendered_append_dependencies <- function(input, # always UTF-8
199260
package_desc <- read.dcf(file.path(package_dir, "DESCRIPTION"),
200261
all = TRUE)
201262
dependency$package <- package_desc$Package
263+
# named to something that doesn't start with 'package' to deter lazy name matching
264+
dependency$pkgVersion <- package_desc$Version
202265
dependency$src$file <- normalized_relative_to(package_dir,
203266
dependency$src$file)
204267
}
@@ -225,6 +288,14 @@ shiny_prerendered_append_dependencies <- function(input, # always UTF-8
225288
# write deps to connection
226289
dependencies_json <- jsonlite::serializeJSON(dependencies, pretty = FALSE)
227290
shiny_prerendered_append_context(con, "dependencies", dependencies_json)
291+
292+
# write r major version and execution package dependencies
293+
execution_json <- jsonlite::serializeJSON(
294+
# visibly display what is being stored
295+
shiny_prerendered_dependencies["packages"],
296+
pretty = FALSE
297+
)
298+
shiny_prerendered_append_context(con, "execution_dependencies", execution_json)
228299
}
229300

230301

@@ -324,7 +395,7 @@ shiny_prerendered_option_hook <- function(input, encoding) {
324395
options$cache > 0)
325396
data_file <- to_utf8(data_file, encoding)
326397
data_dir <- shiny_prerendered_data_dir(input, create = TRUE)
327-
index_file <- shiny_prerendred_data_chunks_index(data_dir)
398+
index_file <- shiny_prerendered_data_chunks_index(data_dir)
328399
conn <- file(index_file, open = "ab", encoding = "UTF-8")
329400
on.exit(close(conn), add = TRUE)
330401
write(data_file, file = conn, append = TRUE)
@@ -406,7 +477,7 @@ shiny_prerendered_evaluate_hook <- function(input) {
406477
shiny_prerendered_remove_uncached_data <- function(input) {
407478
data_dir <- shiny_prerendered_data_dir(input)
408479
if (dir_exists(data_dir)) {
409-
index_file <- shiny_prerendred_data_chunks_index(data_dir)
480+
index_file <- shiny_prerendered_data_chunks_index(data_dir)
410481
if (file.exists(index_file))
411482
unlink(index_file)
412483
rdata_files <- list.files(data_dir, pattern = utils::glob2rx("*.RData"))
@@ -562,7 +633,7 @@ shiny_prerendered_data_load <- function(input_rmd, server_envir) {
562633
data_dir <- shiny_prerendered_data_dir(input_rmd)
563634
if (dir_exists(data_dir)) {
564635
# read index of data files
565-
index_file <- shiny_prerendred_data_chunks_index(data_dir)
636+
index_file <- shiny_prerendered_data_chunks_index(data_dir)
566637
if (file.exists(index_file)) {
567638
rdata_files <- readLines(index_file, encoding = "UTF-8")
568639
# load each of the files in the index
@@ -576,7 +647,7 @@ shiny_prerendered_data_load <- function(input_rmd, server_envir) {
576647
}
577648

578649
# File used to store names of chunks which had cache=TRUE during the last render
579-
shiny_prerendred_data_chunks_index <- function(data_dir) {
650+
shiny_prerendered_data_chunks_index <- function(data_dir) {
580651
file.path(data_dir, "data_chunks_index.txt")
581652
}
582653

@@ -585,4 +656,3 @@ shiny_prerendered_data_file_name <- function(label, cache) {
585656
type <- ifelse(cache, ".cached", "")
586657
sprintf("%s%s.RData", label, type)
587658
}
588-

R/util.R

+24
Original file line numberDiff line numberDiff line change
@@ -513,3 +513,27 @@ package_root <- function(path) {
513513
length(grep('^Package: ', readLines(desc))) == 0) return(package_root(dir))
514514
dir
515515
}
516+
517+
518+
# retrieve package version without fear of error
519+
# loading namespace is ok as these packages have been or will be used
520+
get_package_version_string <- function(package) {
521+
tryCatch(
522+
as.character(getNamespaceVersion(package)),
523+
error = function(e) {
524+
NULL
525+
}
526+
)
527+
}
528+
# find all loaded packages.
529+
# May contain extra packages, but will contain all packages used while knitting
530+
get_loaded_packages <- function() {
531+
packages <- sort(loadedNamespaces())
532+
version <- vapply(packages, get_package_version_string, character(1))
533+
534+
data.frame(
535+
packages = packages,
536+
version = version,
537+
row.names = NULL, stringsAsFactors = FALSE
538+
)
539+
}

0 commit comments

Comments
 (0)