Skip to content

Commit

Permalink
Merge pull request #572 from rstudio/toph-writeManifest-for-quarto-co…
Browse files Browse the repository at this point in the history
…ntent

`rsconnect::writeManifest` for Quarto content
  • Loading branch information
toph-allen authored Apr 15, 2022
2 parents 9c72d5e + 841a396 commit f19621c
Show file tree
Hide file tree
Showing 24 changed files with 578 additions and 67 deletions.
122 changes: 92 additions & 30 deletions R/bundle.R
Original file line number Diff line number Diff line change
Expand Up @@ -222,15 +222,22 @@ enforceBundleLimits <- function(appDir, totalSize, totalFiles) {
bundleApp <- function(appName, appDir, appFiles, appPrimaryDoc, assetTypeName,
contentCategory, verbose = FALSE, python = NULL,
condaMode = FALSE, forceGenerate = FALSE, quarto = NULL,
isShinyApps = FALSE) {
isShinyApps = FALSE, metadata = list()) {
logger <- verboseLogger(verbose)

quartoInfo <- inferQuartoInfo(
appDir = appDir,
appPrimaryDoc = appPrimaryDoc,
quarto = quarto,
metadata = NULL
)

logger("Inferring App mode and parameters")
appMode <- inferAppMode(
appDir = appDir,
appPrimaryDoc = appPrimaryDoc,
files = appFiles,
quarto = quarto)
quartoInfo = quartoInfo)
appPrimaryDoc <- inferAppPrimaryDoc(
appPrimaryDoc = appPrimaryDoc,
appFiles = appFiles,
Expand Down Expand Up @@ -273,7 +280,7 @@ bundleApp <- function(appName, appDir, appFiles, appPrimaryDoc, assetTypeName,
python = python,
hasPythonRmd = hasPythonRmd,
retainPackratDirectory = TRUE,
quarto,
quartoInfo = quartoInfo,
isShinyApps = isShinyApps,
verbose = verbose)
manifestJson <- enc2utf8(toJSON(manifest, pretty = TRUE))
Expand Down Expand Up @@ -343,34 +350,35 @@ detectLongNames <- function(bundleDir, lengthLimit = 32) {

#' Create a manifest.json describing deployment requirements.
#'
#' Given a directory content targeted for deployment, write a manifest.json
#' into that directory describing the deployment requirements for that
#' content.
#' Given a directory content targeted for deployment, write a manifest.json into
#' that directory describing the deployment requirements for that content.
#'
#' @param appDir Directory containing the content (Shiny application, R
#' Markdown document, etc).
#' @param appDir Directory containing the content (Shiny application, R Markdown
#' document, etc).
#'
#' @param appFiles Optional. The full set of files and directories to be
#' included in future deployments of this content. Used when computing
#' dependency requirements. When `NULL`, all files in `appDir` are
#' considered.
#' dependency requirements. When `NULL`, all files in `appDir` are considered.
#'
#' @param appPrimaryDoc Optional. Specifies the primary document in a content
#' directory containing more than one. If `NULL`, the primary document is
#' inferred from the file list.
#'
#' @param contentCategory Optional. Specifies the kind of content being
#' deployed (e.g. `"plot"` or `"site"`).
#' @param contentCategory Optional. Specifies the kind of content being deployed
#' (e.g. `"plot"` or `"site"`).
#'
#' @param python Full path to a python binary for use by `reticulate`.
#' The specified python binary will be invoked to determine its version
#' and to list the python packages installed in the environment.
#' If python = NULL, and RETICULATE_PYTHON is set in the environment,
#' its value will be used.
#' @param python Optional. Full path to a Python binary for use by `reticulate`.
#' The specified Python binary will be invoked to determine its version and to
#' list the Python packages installed in the environment. If `python = NULL`,
#' and `RETICULATE_PYTHON` is set in the environment, its value will be used.
#'
#' @param forceGeneratePythonEnvironment Optional. If an existing
#' `requirements.txt` file is found, it will be overwritten when
#' this argument is `TRUE`.
#' `requirements.txt` file is found, it will be overwritten when this argument
#' is `TRUE`.
#'
#' @param quarto Optional. Full path to a Quarto binary for use deploying Quarto
#' content. The provided Quarto binary will be used to run `quarto inspect`
#' to gather information about the content.
#'
#' @param verbose If TRUE, prints progress messages to the console
#'
Expand All @@ -382,6 +390,7 @@ writeManifest <- function(appDir = getwd(),
contentCategory = NULL,
python = NULL,
forceGeneratePythonEnvironment = FALSE,
quarto = NULL,
verbose = FALSE) {

condaMode <- FALSE
Expand All @@ -392,11 +401,18 @@ writeManifest <- function(appDir = getwd(),
appFiles <- explodeFiles(appDir, appFiles)
}

quartoInfo <- inferQuartoInfo(
appDir = appDir,
appPrimaryDoc = appPrimaryDoc,
quarto = quarto,
metadata = NULL
)

appMode <- inferAppMode(
appDir = appDir,
appPrimaryDoc = appPrimaryDoc,
files = appFiles,
quarto = NULL)
quartoInfo = quartoInfo)
appPrimaryDoc <- inferAppPrimaryDoc(
appPrimaryDoc = appPrimaryDoc,
appFiles = appFiles,
Expand Down Expand Up @@ -433,7 +449,7 @@ writeManifest <- function(appDir = getwd(),
python = python,
hasPythonRmd = hasPythonRmd,
retainPackratDirectory = FALSE,
quarto = NULL,
quartoInfo = quartoInfo,
isShinyApps = FALSE,
verbose = verbose)
manifestJson <- enc2utf8(toJSON(manifest, pretty = TRUE))
Expand Down Expand Up @@ -545,7 +561,7 @@ isShinyRmd <- function(filename) {

# infer the mode of the application from its layout
# unless we're an API, in which case, we're API mode.
inferAppMode <- function(appDir, appPrimaryDoc, files, quarto) {
inferAppMode <- function(appDir, appPrimaryDoc, files, quartoInfo) {
# plumber API
plumberFiles <- grep("^(plumber|entrypoint).r$", files, ignore.case = TRUE, perl = TRUE)
if (length(plumberFiles) > 0) {
Expand All @@ -570,7 +586,7 @@ inferAppMode <- function(appDir, appPrimaryDoc, files, quarto) {

# An Rmd file with a Shiny runtime uses rmarkdown::run.
if (any(shinyRmdFiles)) {
if (is.null(quarto)) {
if (is.null(quartoInfo)) {
return("rmd-shiny")
} else {
return("quarto-shiny")
Expand All @@ -587,7 +603,7 @@ inferAppMode <- function(appDir, appPrimaryDoc, files, quarto) {

# Any non-Shiny R Markdown documents are rendered content (rmd-static).
if (length(rmdFiles) > 0) {
if (is.null(quarto)) {
if (is.null(quartoInfo)) {
return("rmd-static")
} else {
return("quarto-static")
Expand Down Expand Up @@ -643,7 +659,7 @@ inferAppPrimaryDoc <- function(appPrimaryDoc, appFiles, appMode) {
}

## check for extra dependencies congruent to application mode
inferDependencies <- function(appMode, hasParameters, python, hasPythonRmd, quarto) {
inferDependencies <- function(appMode, hasParameters, python, hasPythonRmd, quartoInfo) {
deps <- c()
if (appMode == "rmd-static") {
if (hasParameters) {
Expand All @@ -654,7 +670,7 @@ inferDependencies <- function(appMode, hasParameters, python, hasPythonRmd, quar
}
if (appMode == "quarto-static") {
# Quarto documents need R when the knitr execution engine is used, not always.
if (!is.null(quarto) && "knitr" %in% quarto[["engines"]]) {
if (!is.null(quartoInfo) && "knitr" %in% quartoInfo[["engines"]]) {
deps <- c(deps, "rmarkdown")
}
}
Expand Down Expand Up @@ -747,7 +763,7 @@ createAppManifest <- function(appDir, appMode, contentCategory, hasParameters,
appPrimaryDoc, assetTypeName, users, condaMode,
forceGenerate, python = NULL, hasPythonRmd = FALSE,
retainPackratDirectory = TRUE,
quarto = NULL,
quartoInfo = NULL,
isShinyApps = FALSE,
verbose = FALSE) {

Expand All @@ -765,7 +781,7 @@ createAppManifest <- function(appDir, appMode, contentCategory, hasParameters,
!identical(appMode, "tensorflow-saved-model")) {

# detect dependencies including inferred dependencies
inferredDependencies <- inferDependencies(appMode, hasParameters, python, hasPythonRmd, quarto)
inferredDependencies <- inferDependencies(appMode, hasParameters, python, hasPythonRmd, quartoInfo)
deps = snapshotDependencies(appDir, inferredDependencies, verbose = verbose)

# construct package list from dependencies
Expand Down Expand Up @@ -887,8 +903,8 @@ createAppManifest <- function(appDir, appMode, contentCategory, hasParameters,
manifest$metadata <- metadata

# indicate whether this is a quarto app/doc
if (!is.null(quarto) && !isShinyApps) {
manifest$quarto <- quarto
if (!is.null(quartoInfo) && !isShinyApps) {
manifest$quarto <- quartoInfo
}
# if there is python info for reticulate, attach it
if (!is.null(pyInfo)) {
Expand Down Expand Up @@ -1119,3 +1135,49 @@ performPackratSnapshot <- function(bundleDir, verbose = FALSE) {
# TRUE just to indicate success
TRUE
}

# Run "quarto inspect" on the target and returns its output as a parsed object.
quartoInspect <- function(appDir = NULL, appPrimaryDoc = NULL, quarto = NULL) {
if (!is.null(quarto)) {
inspect <- NULL
# If "quarto inspect appDir" fails, we will try "quarto inspect
# appPrimaryDoc", so that we can support single files as well as projects.
primaryDocPath <- file.path(appDir, appPrimaryDoc) # prior art: appHasParameters()
for (path in c(appDir, primaryDocPath)) {
args <- c("inspect", path.expand(path))
inspect <- suppressWarnings(system2(quarto, args, stdout = TRUE, stderr = FALSE))
if (is.null(attr(inspect, "status"))) {
return(jsonlite::fromJSON(inspect))
}
}
}
return(NULL)
}

# Attempt to gather Quarto version and engines, first from quarto inspect if a
# quarto executable is provided, and then from metadata.
inferQuartoInfo <- function(appDir, appPrimaryDoc, quarto, metadata = NULL) {
quartoInfo <- NULL
if (!is.null(metadata)) {
# Prefer metadata, because that means someone already ran quarto inspect
quartoInfo <- list(
"version" = metadata[["quarto_version"]],
"engines" = metadata[["quarto_engines"]]
)
}
if (is.null(quartoInfo) && !is.null(quarto)) {
# If we don't yet have Quarto details, run quarto inspect ourselves
inspect <- quartoInspect(
appDir = appDir,
appPrimaryDoc = appPrimaryDoc,
quarto = quarto
)
if (!is.null(inspect)) {
quartoInfo <- list(
version = inspect[["quarto"]][["version"]],
engines = I(inspect[["engines"]])
)
}
}
return(quartoInfo)
}
25 changes: 11 additions & 14 deletions R/deployApp.R
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@
#' @param forceGeneratePythonEnvironment Optional. If an existing
#' `requirements.txt` file is found, it will be overwritten when this argument
#' is `TRUE`.
#' @param quarto Optional. Full path to a Quarto binary for use deploying Quarto
#' content. The provided Quarto binary will be used to run `quarto inspect`
#' to gather information about the content.
#' @param appVisibility One of `NULL`, "private"`, or `"public"`; the
#' visibility of the deployment. When `NULL`, no change to visibility is
#' made. Currently has an effect only on deployments to shinyapps.io.
Expand All @@ -93,6 +96,10 @@
#'
#' # deploy but don't launch a browser when completed
#' deployApp(launch.browser = FALSE)
#'
#' # deploy a Quarto website, using the quarto package to
#' # find the Quarto binary
#' deployApp("~/projects/quarto/site1", quarto = quarto::quarto_path())
#' }
#' @seealso [applications()], [terminateApp()], and [restartApp()]
#' @family Deployment functions
Expand All @@ -112,13 +119,14 @@ deployApp <- function(appDir = getwd(),
recordDir = NULL,
launch.browser = getOption("rsconnect.launch.browser",
interactive()),
on.failure = NULL,
logLevel = c("normal", "quiet", "verbose"),
lint = TRUE,
metadata = list(),
forceUpdate = getOption("rsconnect.force.update.apps", FALSE),
python = NULL,
on.failure = NULL,
forceGeneratePythonEnvironment = FALSE,
quarto = NULL,
appVisibility = NULL
) {

Expand Down Expand Up @@ -383,11 +391,10 @@ deployApp <- function(appDir = getwd(),

# python is enabled on Connect but not on Shinyapps
python <- getPythonForTarget(python, accountDetails)
quarto <- getQuartoManifestDetails(metadata)
bundlePath <- bundleApp(target$appName, appDir, appFiles,
appPrimaryDoc, assetTypeName, contentCategory, verbose, python,
condaMode, forceGeneratePythonEnvironment,
quarto, isShinyapps(accountDetails$server))
condaMode, forceGeneratePythonEnvironment, quarto,
isShinyapps(accountDetails$server), metadata)

if (isShinyapps(accountDetails$server)) {

Expand Down Expand Up @@ -520,16 +527,6 @@ getPythonForTarget <- function(path, accountDetails) {
}
}

getQuartoManifestDetails <- function(metadata = list()) {
if (!is.null(metadata[["quarto_version"]])) {
return(list(
"version" = metadata[["quarto_version"]],
"engines" = metadata[["quarto_engines"]]
))
}
return(NULL)
}

# calculate the deployment target based on the passed parameters and
# any saved deployments that we have
deploymentTarget <- function(appPath, appName, appTitle, appId, account,
Expand Down
17 changes: 13 additions & 4 deletions man/deployApp.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit f19621c

Please sign in to comment.