diff --git a/DESCRIPTION b/DESCRIPTION index 38ade75f1..b43ec4d4c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -50,7 +50,7 @@ Suggests: httr2 (>= 1.0.0), knitr (>= 1.39), lintr (>= 3.0.0), - quarto, + quarto (>= 1.5.1), remotes (>= 2.5.0), rmarkdown (>= 2.14), rstudioapi (>= 0.13), diff --git a/NEWS.md b/NEWS.md index 7c85702e2..b9e6afa76 100644 --- a/NEWS.md +++ b/NEWS.md @@ -13,6 +13,7 @@ Deprecations Other improvements +* `build_readme()` gains support for `README.qmd` and renders using Quarto (#2620). * `install()` now installs dependencies with `pak::local_install_deps()` instead of `remotes::install_deps()`. This lets us default to `upgrade = FALSE`, so that existing dependencies are only upgraded when a newer version is actually required (#2486). `keep_source` now defaults to `TRUE` when `build = FALSE`, so that source references are automatically preserved during development installs. * `build_manual()` reports more details on failure (#2586). * `build_site()` now just calls `pkgdown::build_site()`, meaning that you will get more (informative) output by default (#2578). @@ -28,8 +29,7 @@ Other improvements # devtools 2.4.6 -* Functions that use httr now explicitly check that it is installed - (@catalamarti, #2573). +* Functions that use httr now explicitly check that it is installed (@catalamarti, #2573). * `test_coverage()` now works if the package has not been installed. diff --git a/R/build-readme.R b/R/build-readme.R index 2a7d4f94b..0e2351b17 100644 --- a/R/build-readme.R +++ b/R/build-readme.R @@ -4,7 +4,7 @@ #' `r lifecycle::badge("deprecated")` #' #' `build_rmd()` is deprecated, as it is a low-level helper for internal use. To -#' render your package's `README.Rmd` or `README.qmd`, use [build_readme()]. To +#' render your package's `README.qmd` or `README.Rmd`, use [build_readme()]. To #' preview a vignette or article, use functions like [pkgdown::build_site()] or #' [pkgdown::build_article()]. #' @@ -64,9 +64,6 @@ build_rmd_impl <- function( output_options$html_preview <- FALSE for (path in paths) { - if (!quiet) { - cli::cli_inform(c(i = "Building {.path {path}}")) - } callr::r_safe( function(...) rmarkdown::render(...), args = list( @@ -86,36 +83,77 @@ build_rmd_impl <- function( #' Build README #' -#' Renders an executable README, such as `README.Rmd`, to `README.md`. -#' Specifically, `build_readme()`: +#' Renders an executable README, i.e. `README.qmd` or `README.Rmd`, to +#' `README.md`. Specifically, `build_readme()`: #' * Installs a copy of the package's current source to a temporary library #' * Renders the README in a clean R session #' -#' @param path Path to the package to build the README. +#' @param path Path to the top-level directory of the source package. #' @param quiet If `TRUE`, suppresses most output. Set to `FALSE` #' if you need to debug. -#' @param ... Additional arguments passed to [rmarkdown::render()]. +#' @param ... Additional arguments passed to [rmarkdown::render()], in the +#' case of `README.Rmd`. Not used for `README.qmd` #' @export build_readme <- function(path = ".", quiet = TRUE, ...) { pkg <- as.package(path) - regexp <- paste0(path_file(pkg$path), "/(inst/)?readme[.]rmd$") - readme_path <- path_abs(dir_ls( - pkg$path, - ignore.case = TRUE, - regexp = regexp, - recurse = 1, - type = "file" - )) + readme_candidates <- c( + path(pkg$path, "README.qmd"), + path(pkg$path, "README.Rmd"), + path(pkg$path, "inst", "README.qmd"), + path(pkg$path, "inst", "README.Rmd") + ) + readme_path <- readme_candidates[file_exists(readme_candidates)] if (length(readme_path) == 0) { - cli::cli_abort("Can't find {.file README.Rmd} or {.file inst/README.Rmd}.") + cli::cli_abort( + "Can't find {.file README.qmd} or {.file README.Rmd}, at the top-level or + below {.file inst/}." + ) } if (length(readme_path) > 1) { + rel_paths <- path_rel(readme_path, pkg$path) cli::cli_abort( - "Can't have both {.file README.Rmd} and {.file inst/README.Rmd}." + "Found multiple executable READMEs: {.file {rel_paths}}. There can only be + one." ) } - build_rmd_impl(readme_path, path = path, quiet = quiet, ...) + if (!quiet) { + cli::cli_inform(c(i = "Building {.path {readme_path}}")) + } + + if (path_ext(readme_path) == "qmd") { + build_qmd_readme(readme_path, path = path, quiet = quiet) + } else { + build_rmd_impl(readme_path, path = path, quiet = quiet, ...) + } +} + +build_qmd_readme <- function(readme_path, path = ".", quiet = TRUE) { + pkg <- as.package(path) + + check_installed("quarto") + save_all() + + local_install(pkg, quiet = TRUE) + + # Quarto spawns its own R process for knitr, which won't inherit .libPaths(). + + # Pass library paths via R_LIBS_USER so the quarto subprocess finds the + # temporarily installed package first, ahead of any user-installed version. + lib_paths <- paste(.libPaths(), collapse = .Platform$path.sep) + + callr::r_safe( + function(input, quiet) { + quarto::quarto_render(input = input, quiet = quiet) + }, + args = list(input = readme_path, quiet = quiet), + env = c(callr::rcmd_safe_env(), R_LIBS_USER = lib_paths), + show = TRUE, + spinner = FALSE, + stderr = "2>&1" + ) + + invisible(TRUE) } diff --git a/man/build_readme.Rd b/man/build_readme.Rd index 048f97e5c..ffcc4c76a 100644 --- a/man/build_readme.Rd +++ b/man/build_readme.Rd @@ -7,16 +7,17 @@ build_readme(path = ".", quiet = TRUE, ...) } \arguments{ -\item{path}{Path to the package to build the README.} +\item{path}{Path to the top-level directory of the source package.} \item{quiet}{If \code{TRUE}, suppresses most output. Set to \code{FALSE} if you need to debug.} -\item{...}{Additional arguments passed to \code{\link[rmarkdown:render]{rmarkdown::render()}}.} +\item{...}{Additional arguments passed to \code{\link[rmarkdown:render]{rmarkdown::render()}}, in the +case of \code{README.Rmd}. Not used for \code{README.qmd}} } \description{ -Renders an executable README, such as \code{README.Rmd}, to \code{README.md}. -Specifically, \code{build_readme()}: +Renders an executable README, i.e. \code{README.qmd} or \code{README.Rmd}, to +\code{README.md}. Specifically, \code{build_readme()}: \itemize{ \item Installs a copy of the package's current source to a temporary library \item Renders the README in a clean R session diff --git a/man/build_rmd.Rd b/man/build_rmd.Rd index f3266f0ba..c55571d08 100644 --- a/man/build_rmd.Rd +++ b/man/build_rmd.Rd @@ -25,7 +25,7 @@ format is read from metadata (i.e. not a custom format object passed to \ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#deprecated}{\figure{lifecycle-deprecated.svg}{options: alt='[Deprecated]'}}}{\strong{[Deprecated]}} \code{build_rmd()} is deprecated, as it is a low-level helper for internal use. To -render your package's \code{README.Rmd} or \code{README.qmd}, use \code{\link[=build_readme]{build_readme()}}. To +render your package's \code{README.qmd} or \code{README.Rmd}, use \code{\link[=build_readme]{build_readme()}}. To preview a vignette or article, use functions like \code{\link[pkgdown:build_site]{pkgdown::build_site()}} or \code{\link[pkgdown:build_articles]{pkgdown::build_article()}}. } diff --git a/tests/testthat/_snaps/build-readme.md b/tests/testthat/_snaps/build-readme.md index 841d36431..e37ab3754 100644 --- a/tests/testthat/_snaps/build-readme.md +++ b/tests/testthat/_snaps/build-readme.md @@ -4,7 +4,7 @@ build_readme(pkg) Condition Error in `build_readme()`: - ! Can't find 'README.Rmd' or 'inst/README.Rmd'. + ! Can't find 'README.qmd' or 'README.Rmd', at the top-level or below 'inst/'. --- @@ -12,7 +12,15 @@ build_readme(pkg) Condition Error in `build_readme()`: - ! Can't have both 'README.Rmd' and 'inst/README.Rmd'. + ! Found multiple executable READMEs: 'README.Rmd' and 'inst/README.Rmd'. There can only be one. + +# errors if both README.qmd and README.Rmd exist + + Code + build_readme(pkg) + Condition + Error in `build_readme()`: + ! Found multiple executable READMEs: 'README.qmd' and 'README.Rmd'. There can only be one. # build_rmd() is deprecated diff --git a/tests/testthat/test-build-readme.R b/tests/testthat/test-build-readme.R index 8afb381a0..34c1ad7ac 100644 --- a/tests/testthat/test-build-readme.R +++ b/tests/testthat/test-build-readme.R @@ -1,4 +1,4 @@ -test_that("can build README in root directory", { +test_that("can build README.Rmd in root directory", { skip_on_cran() pkg <- local_package_create() @@ -14,7 +14,7 @@ test_that("can build README in root directory", { expect_false(file_exists(path(pkg, "README.html"))) }) -test_that("can build README in inst/", { +test_that("can build README.Rmd in inst/", { skip_on_cran() pkg <- local_package_create() @@ -37,6 +37,59 @@ test_that("can build README in inst/", { expect_false(file_exists(path(pkg, "inst", "README.html"))) }) +test_that("can build README.qmd in root directory", { + skip_on_cran() + skip_if_not_installed("quarto") + skip_if_not(quarto::quarto_available(), "quarto cli not available") + + pkg <- local_package_create() + # TODO: use usethis::use_readme_qmd() once it's in a usethis release + # https://github.com/r-lib/usethis/pull/2219 + writeLines( + c( + "---", + "format: gfm", + "---", + "", + "# testpkg", + "", + "This is a test package." + ), + path(pkg, "README.qmd") + ) + + build_readme(pkg, quiet = TRUE) + expect_true(file_exists(path(pkg, "README.md"))) +}) + +test_that("can build README.qmd in inst/", { + skip_on_cran() + skip_if_not_installed("quarto") + skip_if_not(quarto::quarto_available(), "quarto cli not available") + + pkg <- local_package_create() + # TODO: use usethis::use_readme_qmd() once it's in a usethis release + # https://github.com/r-lib/usethis/pull/2219 + dir_create(pkg, "inst") + writeLines( + c( + "---", + "format: gfm", + "---", + "", + "# testpkg", + "", + "This is a test package." + ), + path(pkg, "inst", "README.qmd") + ) + + build_readme(pkg, quiet = TRUE) + expect_true(file_exists(path(pkg, "inst", "README.md"))) + expect_false(file_exists(path(pkg, "README.qmd"))) + expect_false(file_exists(path(pkg, "README.md"))) +}) + test_that("useful errors if too few or too many", { pkg <- local_package_create() expect_snapshot(build_readme(pkg), error = TRUE) @@ -52,6 +105,13 @@ test_that("useful errors if too few or too many", { expect_snapshot(build_readme(pkg), error = TRUE) }) +test_that("errors if both README.qmd and README.Rmd exist", { + pkg <- local_package_create() + file_create(path(pkg, "README.Rmd")) + file_create(path(pkg, "README.qmd")) + expect_snapshot(build_readme(pkg), error = TRUE) +}) + test_that("don't error for README in another directory", { skip_on_cran()