From 281d3d001ca8b44b5076fab3cdb9c2dac67c45e9 Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Sun, 11 Jan 2026 11:20:03 +0100 Subject: [PATCH 1/5] Report the LaTeX error from stderr when build_manual() fails. This parses the stderr text for the "LaTeX Error:" line and reports it if it's found. --- R/build-manual.R | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/R/build-manual.R b/R/build-manual.R index 0c9112db2..4b041e50d 100644 --- a/R/build-manual.R +++ b/R/build-manual.R @@ -17,7 +17,10 @@ build_manual <- function(pkg = ".", path = NULL) { ), fail_on_status = TRUE, stderr = "2>&1", spinner = FALSE), error = function(e) { cat(e$stdout) - cli::cli_abort("Failed to build manual") + msg <- regmatches(e$stderr, + regexpr("LaTeX Error:.*", e$stderr, perl = TRUE)) + cli::cli_abort(c("x" = msg, + "!" = "Failed to build manual")) }) cat(msg$stdout) From b54eafd40b08a587e3f1ac555e4fa254bd6f342a Mon Sep 17 00:00:00 2001 From: Marco Colombo Date: Mon, 12 Jan 2026 23:17:31 +0100 Subject: [PATCH 2/5] Print the entire stderr buffer in case of error. --- R/build-manual.R | 6 ++---- R/utils.R | 7 +++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/R/build-manual.R b/R/build-manual.R index 4b041e50d..923a703b1 100644 --- a/R/build-manual.R +++ b/R/build-manual.R @@ -14,12 +14,10 @@ build_manual <- function(pkg = ".", path = NULL) { "--force", paste0("--output=", path, "/", name), pkg$path - ), fail_on_status = TRUE, stderr = "2>&1", spinner = FALSE), + ), fail_on_status = TRUE, spinner = FALSE), error = function(e) { cat(e$stdout) - msg <- regmatches(e$stderr, - regexpr("LaTeX Error:.*", e$stderr, perl = TRUE)) - cli::cli_abort(c("x" = msg, + cli::cli_abort(c("x" = no_wrap(e$stderr), "!" = "Failed to build manual")) }) diff --git a/R/utils.R b/R/utils.R index 3ea06488a..105a2c7d1 100644 --- a/R/utils.R +++ b/R/utils.R @@ -44,3 +44,10 @@ is_testing <- function() { is_rstudio_running <- function() { !is_testing() && rstudioapi::isAvailable() } + +# Suppress cli wrapping +no_wrap <- function(x) { + x <- gsub(" ", "\u00a0", x, fixed = TRUE) + x <- gsub("\n", "\f", x, fixed = TRUE) + x +} From 061b0bb0cb66d206fcf092d1023bdcc035064fb4 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Wed, 21 Jan 2026 11:15:25 -0600 Subject: [PATCH 3/5] Add basic test; polish `build_manual()` --- NEWS.md | 1 + R/build-manual.R | 25 ++++++++++++------------- tests/testthat/_snaps/build-manual.md | 11 +++++++++++ tests/testthat/test-build-manual.R | 19 +++++++++++++++++++ 4 files changed, 43 insertions(+), 13 deletions(-) create mode 100644 tests/testthat/_snaps/build-manual.md create mode 100644 tests/testthat/test-build-manual.R diff --git a/NEWS.md b/NEWS.md index 698c98a8d..2b5fb213c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,6 @@ # devtools (development version) +* `build_manual()` reports more details on failure (#2586). * `is_loading()` is now re-exported from pkgload (#2556). * `load_all()` now errors if called recursively, i.e. if you accidentally include a `load_all()` call in one of your R source files (#2617). diff --git a/R/build-manual.R b/R/build-manual.R index 573fe2e23..452f0ecff 100644 --- a/R/build-manual.R +++ b/R/build-manual.R @@ -9,24 +9,23 @@ build_manual <- function(pkg = ".", path = NULL) { pkg <- as.package(pkg) path <- path %||% path_dir(pkg$path) - name <- paste0(pkg$package, "_", pkg$version, ".pdf", collapse = " ") + name <- paste0(pkg$package, "_", pkg$version, ".pdf") + output <- file.path(path, name) + + cli::cli_inform("Saving manual to {.file {output}}") tryCatch( - msg <- callr::rcmd( + invisible(callr::rcmd( "Rd2pdf", - cmdargs = c( - "--force", - paste0("--output=", path, "/", name), - pkg$path - ), + cmdargs = c("--force", paste0("--output=", output), pkg$path), + stdout = "", fail_on_status = TRUE, spinner = FALSE - ), + )), error = function(e) { - cat(e$stdout) - cli::cli_abort(c("x" = no_wrap(e$stderr), "!" = "Failed to build manual")) + cli::cli_abort( + c("Failed to build manual.", no_wrap(e$stderr)), + call = quote(build_manual()) + ) } ) - - cat(msg$stdout) - invisible(msg) } diff --git a/tests/testthat/_snaps/build-manual.md b/tests/testthat/_snaps/build-manual.md new file mode 100644 index 000000000..2bf08fd80 --- /dev/null +++ b/tests/testthat/_snaps/build-manual.md @@ -0,0 +1,11 @@ +# build_manual() shows stderr on failure + + Code + build_manual(pkg) + Message + Saving manual to '_0.0.0.9000.pdf' + Condition + Error in `build_manual()`: + ! Failed to build manual. + ! LaTeX Error: File `inconsolata.sty' not found. + diff --git a/tests/testthat/test-build-manual.R b/tests/testthat/test-build-manual.R new file mode 100644 index 000000000..cdb24c106 --- /dev/null +++ b/tests/testthat/test-build-manual.R @@ -0,0 +1,19 @@ +test_that("build_manual() shows stderr on failure", { + pkg <- local_package_create() + pkg <- normalizePath(pkg) + + # Create a mock error similar to what callr::rcmd produces + mock_error <- function(...) { + cnd <- rlang::error_cnd( + message = "System command 'R' failed", + stderr = "! LaTeX Error: File `inconsolata.sty' not found." + ) + stop(cnd) + } + local_mocked_bindings(rcmd = mock_error, .package = "callr") + + expect_snapshot(build_manual(pkg), error = TRUE, transform = function(x) { + x <- gsub(pkg, "", x, fixed = TRUE) + x + }) +}) From d7ec69f04e9cefd6c7011fb9ed6b0d5d2995adf9 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Wed, 21 Jan 2026 11:32:57 -0600 Subject: [PATCH 4/5] More polishing --- R/build-manual.R | 23 ++++++++++++++--------- tests/testthat/_snaps/build-manual.md | 2 ++ tests/testthat/test-build-manual.R | 14 +++++++------- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/R/build-manual.R b/R/build-manual.R index 452f0ecff..301e773e3 100644 --- a/R/build-manual.R +++ b/R/build-manual.R @@ -13,19 +13,24 @@ build_manual <- function(pkg = ".", path = NULL) { output <- file.path(path, name) cli::cli_inform("Saving manual to {.file {output}}") - tryCatch( - invisible(callr::rcmd( - "Rd2pdf", - cmdargs = c("--force", paste0("--output=", output), pkg$path), - stdout = "", - fail_on_status = TRUE, - spinner = FALSE - )), + withCallingHandlers( + invisible(rd2pdf(pkg$path, output)), error = function(e) { cli::cli_abort( c("Failed to build manual.", no_wrap(e$stderr)), - call = quote(build_manual()) + call = quote(build_manual()), + parent = e ) } ) } + +rd2pdf <- function(pkg_path, output_path) { + callr::rcmd( + "Rd2pdf", + cmdargs = c("--force", paste0("--output=", output_path), pkg_path), + stdout = "", + fail_on_status = TRUE, + spinner = FALSE + ) +} diff --git a/tests/testthat/_snaps/build-manual.md b/tests/testthat/_snaps/build-manual.md index 2bf08fd80..a8c212dd0 100644 --- a/tests/testthat/_snaps/build-manual.md +++ b/tests/testthat/_snaps/build-manual.md @@ -8,4 +8,6 @@ Error in `build_manual()`: ! Failed to build manual. ! LaTeX Error: File `inconsolata.sty' not found. + Caused by error in `rd2pdf()`: + ! System command 'R' failed diff --git a/tests/testthat/test-build-manual.R b/tests/testthat/test-build-manual.R index cdb24c106..39bf4aaab 100644 --- a/tests/testthat/test-build-manual.R +++ b/tests/testthat/test-build-manual.R @@ -1,16 +1,16 @@ test_that("build_manual() shows stderr on failure", { + skip_on_os("windows") + pkg <- local_package_create() pkg <- normalizePath(pkg) - # Create a mock error similar to what callr::rcmd produces - mock_error <- function(...) { - cnd <- rlang::error_cnd( - message = "System command 'R' failed", + # Too hard to replicate actual error, so we just simulate + local_mocked_bindings(rd2pdf = function(...) { + rlang::abort( + "System command 'R' failed", stderr = "! LaTeX Error: File `inconsolata.sty' not found." ) - stop(cnd) - } - local_mocked_bindings(rcmd = mock_error, .package = "callr") + }) expect_snapshot(build_manual(pkg), error = TRUE, transform = function(x) { x <- gsub(pkg, "", x, fixed = TRUE) From 64101c68db1bcbd1d5a5ffd8922fcd9fd4daf3d3 Mon Sep 17 00:00:00 2001 From: Hadley Wickham Date: Wed, 21 Jan 2026 12:32:45 -0600 Subject: [PATCH 5/5] Silence the lint --- tests/testthat/test-build-manual.R | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/testthat/test-build-manual.R b/tests/testthat/test-build-manual.R index 39bf4aaab..75b64e7ae 100644 --- a/tests/testthat/test-build-manual.R +++ b/tests/testthat/test-build-manual.R @@ -6,10 +6,8 @@ test_that("build_manual() shows stderr on failure", { # Too hard to replicate actual error, so we just simulate local_mocked_bindings(rd2pdf = function(...) { - rlang::abort( - "System command 'R' failed", - stderr = "! LaTeX Error: File `inconsolata.sty' not found." - ) + stderr <- "! LaTeX Error: File `inconsolata.sty' not found." + rlang::abort("System command 'R' failed", stderr = stderr) #nolint }) expect_snapshot(build_manual(pkg), error = TRUE, transform = function(x) {