From 6b2bf3b36f4c72d5d5681d112ede296c31eae1e0 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Mon, 2 Mar 2026 10:54:33 -0800 Subject: [PATCH 1/8] Support README.qmd --- DESCRIPTION | 1 + NEWS.md | 4 +- R/build-readme.R | 60 +++++++++++++++++++++++---- man/build_readme.Rd | 9 ++-- man/build_rmd.Rd | 2 +- tests/testthat/_snaps/build-readme.md | 12 +++++- tests/testthat/test-build-readme.R | 52 ++++++++++++++++++++++- 7 files changed, 120 insertions(+), 20 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index fbe2f203c..4a00781cb 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -50,6 +50,7 @@ Suggests: httr2 (>= 1.0.0), knitr (>= 1.39), lintr (>= 3.0.0), + quarto (>= 1.5.1), remotes (>= 2.5.0), rmarkdown (>= 2.14), rstudioapi (>= 0.13), diff --git a/NEWS.md b/NEWS.md index d26cb782d..152413043 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). @@ -27,8 +28,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..f3cea351b 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()]. #' @@ -86,20 +86,21 @@ 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$") + regexp <- paste0(path_file(pkg$path), "/(inst/)?readme[.](r|q)md$") readme_path <- path_abs(dir_ls( pkg$path, ignore.case = TRUE, @@ -109,13 +110,54 @@ build_readme <- function(path = ".", quiet = TRUE, ...) { )) 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 (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) + + if (!quiet) { + cli::cli_inform(c(i = "Building {.path {readme_path}}")) + } + + # 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, lib_paths) { + withr::local_envvar(R_LIBS_USER = lib_paths) + quarto::quarto_render(input = input, quiet = quiet) + }, + args = list(input = readme_path, quiet = quiet, lib_paths = 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..0771e783a 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.Rmd' and 'README.qmd'. 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..5b9492cf3 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,47 @@ 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(!nzchar(Sys.which("quarto")), "quarto cli not available") + + pkg <- local_package_create() + usethis::ui_silence( + usethis::with_project( + pkg, + use_readme_qmd(open = FALSE) + ) + ) + + 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(!nzchar(Sys.which("quarto")), "quarto cli not available") + + pkg <- local_package_create() + usethis::ui_silence( + usethis::with_project( + pkg, + use_readme_qmd(open = FALSE) + ) + ) + dir_create(pkg, "inst") + file_move( + path(pkg, "README.qmd"), + 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 +93,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() From f2bf311b011ed6dbb551276f5fb7211d61fd1f1c Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Mon, 2 Mar 2026 11:08:38 -0800 Subject: [PATCH 2/8] Better skip re: quarto --- tests/testthat/test-build-readme.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-build-readme.R b/tests/testthat/test-build-readme.R index 5b9492cf3..6c6771401 100644 --- a/tests/testthat/test-build-readme.R +++ b/tests/testthat/test-build-readme.R @@ -40,7 +40,7 @@ test_that("can build README.Rmd in inst/", { test_that("can build README.qmd in root directory", { skip_on_cran() skip_if_not_installed("quarto") - skip_if(!nzchar(Sys.which("quarto")), "quarto cli not available") + skip_if_not(quarto::quarto_available(), "quarto cli not available") pkg <- local_package_create() usethis::ui_silence( @@ -57,7 +57,7 @@ test_that("can build README.qmd in root directory", { test_that("can build README.qmd in inst/", { skip_on_cran() skip_if_not_installed("quarto") - skip_if(!nzchar(Sys.which("quarto")), "quarto cli not available") + skip_if_not(quarto::quarto_available(), "quarto cli not available") pkg <- local_package_create() usethis::ui_silence( From 688620ea0c9f33d0f332f4c62bc8003972b7dfd5 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Mon, 2 Mar 2026 11:47:25 -0800 Subject: [PATCH 3/8] Make sort order same across OSes --- R/build-readme.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/R/build-readme.R b/R/build-readme.R index f3cea351b..4632c640d 100644 --- a/R/build-readme.R +++ b/R/build-readme.R @@ -101,13 +101,13 @@ build_readme <- function(path = ".", quiet = TRUE, ...) { pkg <- as.package(path) regexp <- paste0(path_file(pkg$path), "/(inst/)?readme[.](r|q)md$") - readme_path <- path_abs(dir_ls( + readme_path <- sort(path_abs(dir_ls( pkg$path, ignore.case = TRUE, regexp = regexp, recurse = 1, type = "file" - )) + )), method = "radix") if (length(readme_path) == 0) { cli::cli_abort( From 62f6deee142339a8c3dfd7240c4c499ff77d6908 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Tue, 3 Mar 2026 14:38:53 -0800 Subject: [PATCH 4/8] Empty commit to trigger CI From 347b41d9635945596636dbf8bc53bfce92e6c7d5 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Tue, 3 Mar 2026 14:51:06 -0800 Subject: [PATCH 5/8] Don't use usethis::use_readme_qmd() (yet) I want to release devtools soon and, in particular, before I release usethis. --- tests/testthat/test-build-readme.R | 38 ++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/tests/testthat/test-build-readme.R b/tests/testthat/test-build-readme.R index 6c6771401..34c1ad7ac 100644 --- a/tests/testthat/test-build-readme.R +++ b/tests/testthat/test-build-readme.R @@ -43,11 +43,19 @@ test_that("can build README.qmd in root directory", { skip_if_not(quarto::quarto_available(), "quarto cli not available") pkg <- local_package_create() - usethis::ui_silence( - usethis::with_project( - pkg, - use_readme_qmd(open = FALSE) - ) + # 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) @@ -60,15 +68,19 @@ test_that("can build README.qmd in inst/", { skip_if_not(quarto::quarto_available(), "quarto cli not available") pkg <- local_package_create() - usethis::ui_silence( - usethis::with_project( - pkg, - use_readme_qmd(open = FALSE) - ) - ) + # 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") - file_move( - path(pkg, "README.qmd"), + writeLines( + c( + "---", + "format: gfm", + "---", + "", + "# testpkg", + "", + "This is a test package." + ), path(pkg, "inst", "README.qmd") ) From f880223985995462357c5eab14669d189854ad3b Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Tue, 3 Mar 2026 15:39:07 -0800 Subject: [PATCH 6/8] Move the cli::cli_inform() up --- R/build-readme.R | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/R/build-readme.R b/R/build-readme.R index 4632c640d..964d1ec29 100644 --- a/R/build-readme.R +++ b/R/build-readme.R @@ -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( @@ -101,13 +98,16 @@ build_readme <- function(path = ".", quiet = TRUE, ...) { pkg <- as.package(path) regexp <- paste0(path_file(pkg$path), "/(inst/)?readme[.](r|q)md$") - readme_path <- sort(path_abs(dir_ls( - pkg$path, - ignore.case = TRUE, - regexp = regexp, - recurse = 1, - type = "file" - )), method = "radix") + readme_path <- sort( + path_abs(dir_ls( + pkg$path, + ignore.case = TRUE, + regexp = regexp, + recurse = 1, + type = "file" + )), + method = "radix" + ) if (length(readme_path) == 0) { cli::cli_abort( @@ -123,6 +123,10 @@ build_readme <- function(path = ".", quiet = TRUE, ...) { ) } + 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 { @@ -138,10 +142,6 @@ build_qmd_readme <- function(readme_path, path = ".", quiet = TRUE) { local_install(pkg, quiet = TRUE) - if (!quiet) { - cli::cli_inform(c(i = "Building {.path {readme_path}}")) - } - # 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 From ae6ba35f46ba129ea2749df904737b5bd382a3fd Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Tue, 3 Mar 2026 15:46:48 -0800 Subject: [PATCH 7/8] Use callr::r_safe(env =) --- R/build-readme.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/build-readme.R b/R/build-readme.R index 964d1ec29..6d164abd6 100644 --- a/R/build-readme.R +++ b/R/build-readme.R @@ -149,11 +149,11 @@ build_qmd_readme <- function(readme_path, path = ".", quiet = TRUE) { lib_paths <- paste(.libPaths(), collapse = .Platform$path.sep) callr::r_safe( - function(input, quiet, lib_paths) { - withr::local_envvar(R_LIBS_USER = lib_paths) + function(input, quiet) { quarto::quarto_render(input = input, quiet = quiet) }, - args = list(input = readme_path, quiet = quiet, lib_paths = lib_paths), + 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" From f41b30edbd664a062695a0988fa5f10acfbdac10 Mon Sep 17 00:00:00 2001 From: Jenny Bryan Date: Tue, 3 Mar 2026 16:03:18 -0800 Subject: [PATCH 8/8] Work from a fixed list of executable README paths --- R/build-readme.R | 16 ++++++---------- tests/testthat/_snaps/build-readme.md | 2 +- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/R/build-readme.R b/R/build-readme.R index 6d164abd6..0e2351b17 100644 --- a/R/build-readme.R +++ b/R/build-readme.R @@ -97,17 +97,13 @@ build_rmd_impl <- function( build_readme <- function(path = ".", quiet = TRUE, ...) { pkg <- as.package(path) - regexp <- paste0(path_file(pkg$path), "/(inst/)?readme[.](r|q)md$") - readme_path <- sort( - path_abs(dir_ls( - pkg$path, - ignore.case = TRUE, - regexp = regexp, - recurse = 1, - type = "file" - )), - method = "radix" + 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( diff --git a/tests/testthat/_snaps/build-readme.md b/tests/testthat/_snaps/build-readme.md index 0771e783a..e37ab3754 100644 --- a/tests/testthat/_snaps/build-readme.md +++ b/tests/testthat/_snaps/build-readme.md @@ -20,7 +20,7 @@ build_readme(pkg) Condition Error in `build_readme()`: - ! Found multiple executable READMEs: 'README.Rmd' and 'README.qmd'. There can only be one. + ! Found multiple executable READMEs: 'README.qmd' and 'README.Rmd'. There can only be one. # build_rmd() is deprecated