Skip to content

Commit 09c8305

Browse files
Skip compiling and generating wrappers when unnecessary (#81)
* Handle compile=NA * Skip generating wrapper-files when unnecessary - add force arg to force generating them - remove force_wrappers * Remove compile from make_wrappers_externally * Tweak * Fix comment * Improve cli expressions * Reflect the changes by #76 * Stop showing a warning * Tweak comments * Implement find_newer_files_than() * Tweak error message * Use `library_path` to match with the function name * Add test about force = TRUE * Fix function name * Fix test * Sleep less furiously * Fix message * Improve the error in find_newer_files_than()
1 parent 27347f2 commit 09c8305

File tree

6 files changed

+166
-80
lines changed

6 files changed

+166
-80
lines changed

.github/workflows/test_pkg_gen.yaml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131

3232
steps:
3333
- uses: actions/checkout@v2
34-
34+
3535
- name: Set up Rust
3636
uses: actions-rs/toolchain@v1
3737
with:
@@ -67,7 +67,7 @@ jobs:
6767
run: |
6868
rustup target add x86_64-pc-windows-gnu
6969
rustup target add i686-pc-windows-gnu
70-
echo "C:\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
70+
echo "C:\msys64\mingw64\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
7171
echo "C:\msys64\mingw32\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
7272
shell: pwsh
7373

@@ -93,6 +93,7 @@ jobs:
9393
env:
9494
_R_CHECK_CRAN_INCOMING_REMOTE_: false
9595
run: |
96+
# preperation
9697
remotes::install_local(force = TRUE)
9798
temp_dir <- tempdir()
9899
pkg_dir <- file.path(temp_dir, "testpkg")
@@ -116,11 +117,35 @@ jobs:
116117
),
117118
file.path("tests", "testthat", "test-hello.R")
118119
)
120+
121+
# check if rextendr::document() compiles and generates wrappers properly
119122
rextendr::document()
120123
rcmdcheck::rcmdcheck(
121-
path = ".",
124+
path = ".",
122125
args = c("--no-manual", "--as-cran"),
123126
error_on = "warning",
124127
check_dir = "check_use_extendr"
125128
)
129+
130+
library_path <- rextendr:::get_library_path()
131+
library_mtime_before <- file.info(library_path)$mtime
132+
wrappers_path <- file.path("R", "extendr-wrappers.R")
133+
wrappers_mtime_before <- file.info(wrappers_path)$mtime
134+
135+
# check if rextendr::document() don't re-generate wrappers when unnecessary
136+
rextendr::document()
137+
stopifnot(library_mtime_before == file.info(library_path)$mtime)
138+
stopifnot(wrappers_mtime_before == file.info(wrappers_path)$mtime)
139+
140+
# check if force = TRUE forces re-generating wrappers, but not compile
141+
rextendr::register_extendr(force = TRUE)
142+
stopifnot(library_mtime_before == file.info(library_path)$mtime)
143+
stopifnot(wrappers_mtime_before < file.info(wrappers_path)$mtime)
144+
145+
wrappers_mtime_before <- file.info(wrappers_path)$mtime
146+
147+
# check if compile = TRUE forces compile, and accordingly the wrapper generation
148+
rextendr::register_extendr(compile = TRUE)
149+
stopifnot(library_mtime_before < file.info(library_path)$mtime)
150+
stopifnot(wrappers_mtime_before < file.info(wrappers_path)$mtime)
126151
shell: Rscript {0}

R/register_extendr.R

Lines changed: 61 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,21 @@
1010
#' `R/extendr-wrappers.R`. Afterwards, you will have to re-document and then
1111
#' re-install the package for the wrapper functions to take effect.
1212
#'
13-
#' @inheritParams pkgload::load_all
1413
#' @param path Path from which package root is looked up.
1514
#' @param quiet Logical indicating whether any progress messages should be
1615
#' generated or not.
17-
#' @param force_wrappers Logical indicating whether to install a minimal wrapper
18-
#' file in the cases when generating wrappers by Rust code failed. This might
19-
#' be needed when the wrapper file is accidentally lost or corrupted.
16+
#' @param force Logical indicating whether to force re-generating
17+
#' `R/extendr-wrappers.R` even when it doesn't seem to need updated. (By
18+
#' default, generation is skipped when it's newer than the DLL).
19+
#' @param compile Logical indicating whether to recompile DLLs:
20+
#' \describe{
21+
#' \item{`TRUE`}{always recompiles}
22+
#' \item{`NA`}{recompiles if needed (i.e., any source files or manifest file are newer than the DLL)}
23+
#' \item{`FALSE`}{never recompiles}
24+
#' }
2025
#' @return (Invisibly) Path to the file containing generated wrappers.
2126
#' @export
22-
register_extendr <- function(path = ".", quiet = FALSE, force_wrappers = FALSE, compile = NA) {
27+
register_extendr <- function(path = ".", quiet = FALSE, force = FALSE, compile = NA) {
2328
x <- desc::desc(rprojroot::find_package_root_file("DESCRIPTION", path = path))
2429
pkg_name <- x$get("Package")
2530

@@ -40,29 +45,51 @@ register_extendr <- function(path = ".", quiet = FALSE, force_wrappers = FALSE,
4045

4146
outfile <- rprojroot::find_package_root_file("R", "extendr-wrappers.R", path = path)
4247

43-
# If force_wrappers is TRUE, use a minimal wrapper file even when
44-
# make_wrappers() fails; since the wrapper generation depends on the compiled
45-
# Rust code, the package needs to be installed before attempting this, but
46-
# it's not always the case (e.g. the package might be corrupted, or not
47-
# installed yet).
48-
if (isTRUE(force_wrappers)) {
49-
error_handle <- function(e) {
50-
cli::cli_alert_danger("Failed to generate wrapper functions: {e$message}.")
51-
cli::cli_alert_warning("Falling back to a minimal wrapper file instead.")
52-
53-
make_example_wrappers(pkg_name, outfile, path = path)
54-
}
55-
} else {
56-
error_handle <- function(e) {
48+
path <- rprojroot::find_package_root_file(path = path)
49+
50+
# If compile is NA, compile if the DLL is newer than the source files
51+
if (isTRUE(is.na(compile))) {
52+
compile <- needs_compilation(path, quiet) || pkgbuild::needs_compile(path)
53+
}
54+
55+
if (isTRUE(compile)) {
56+
# This relies on [`pkgbuild::needs_compile()`], which
57+
# does not know about Rust files modifications.
58+
# `force = TRUE` enforces compilation.
59+
pkgbuild::compile_dll(
60+
path = path,
61+
force = TRUE,
62+
quiet = quiet
63+
)
64+
}
65+
66+
library_path <- get_library_path(path)
67+
68+
if (!file.exists(library_path)) {
69+
msg <- "{library_path} doesn't exist"
70+
if (isTRUE(compile)) {
71+
# If it doesn't exist even after compile, we have no idea what happened
72+
ui_throw(msg)
73+
} else {
74+
# If compile wasn't invoked, it might succeed with explicit "compile = TRUE"
5775
ui_throw(
58-
"Failed to generate wrapper functions.",
59-
c(
60-
ui_x(e[["message"]])
61-
)
76+
msg,
77+
ui_i("You need to compile first, try `register_rextendr(compile = TRUE)`")
6278
)
6379
}
6480
}
6581

82+
# If the wrapper file is newer than the DLL file, assume it's been generated
83+
# by the latest DLL, which should mean it doesn't need to be re-generated.
84+
# This isn't always the case (e.g. when the user accidentally edited the
85+
# wrapper file by hand) so the user might need to run with `force = TRUE`.
86+
if (!isTRUE(force) && length(find_newer_files_than(outfile, library_path)) > 0) {
87+
rel_path <- pretty_rel_path(outfile, path)
88+
cli::cli_alert_info("{.file {rel_path}} is up-to-date. Skip generating wrapper functions.")
89+
90+
return(invisible(character(0L)))
91+
}
92+
6693
tryCatch(
6794
# Call the wrapper generation in a separate R process to avoid the problem
6895
# of loading and unloading the same name of a DLL (c.f. #64).
@@ -72,10 +99,14 @@ register_extendr <- function(path = ".", quiet = FALSE, force_wrappers = FALSE,
7299
outfile = outfile,
73100
path = path,
74101
use_symbols = TRUE,
75-
quiet = quiet,
76-
compile = compile
102+
quiet = quiet
77103
),
78-
error = error_handle
104+
error = function(e) {
105+
ui_throw(
106+
"Failed to generate wrapper functions.",
107+
ui_x(e[["message"]])
108+
)
109+
}
79110
)
80111

81112
# Ensures path is absolute
@@ -121,28 +152,15 @@ make_wrappers <- function(module_name, package_name, outfile,
121152
#'
122153
#' Does the same as [`make_wrappers`], but out of process.
123154
#' @inheritParams make_wrappers
124-
#' @param compile Logical indicating whether the library should be recompiled.
125155
#' @noRd
126156
make_wrappers_externally <- function(module_name, package_name, outfile,
127-
path, use_symbols = FALSE, quiet = FALSE,
128-
compile = NA) {
129-
func <- function(path, make_wrappers, compile, quiet,
157+
path, use_symbols = FALSE, quiet = FALSE) {
158+
func <- function(path, make_wrappers, quiet,
130159
module_name, package_name, outfile,
131160
use_symbols, ...) {
132-
if (isTRUE(compile)) {
133-
# This relies on [`pkgbuild::needs_compile()`], which
134-
# does not know about Rust files modifications.
135-
# `force = TRUE` enforces compilation.
136-
pkgbuild::compile_dll(
137-
path = path,
138-
force = TRUE,
139-
quiet = quiet
140-
)
141-
}
142-
143-
dll_path <- file.path(path, "src", paste0(package_name, .Platform$dynlib.ext))
161+
library_path <- file.path(path, "src", paste0(package_name, .Platform$dynlib.ext))
144162
# Loads native library
145-
lib <- dyn.load(dll_path)
163+
lib <- dyn.load(library_path)
146164
# Registers library unloading to be invoked at the end of this function
147165
on.exit(dyn.unload(lib[["path"]]), add = TRUE)
148166

@@ -159,7 +177,6 @@ make_wrappers_externally <- function(module_name, package_name, outfile,
159177
args <- list(
160178
path = rprojroot::find_package_root_file(path = path),
161179
make_wrappers = make_wrappers,
162-
compile = compile,
163180
# arguments passed to make_wrappers()
164181
module_name = module_name,
165182
package_name = package_name,

R/rextendr_document.R

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
#' @export
1111
document <- function(pkg = ".", quiet = FALSE, roclets = NULL) {
1212
try_save_all(quiet = quiet)
13-
needs_compilation <- needs_compilation(pkg, quiet) || pkgbuild::needs_compile(pkg)
1413

15-
register_extendr(path = pkg, quiet = quiet, compile = needs_compilation)
14+
register_extendr(path = pkg, quiet = quiet)
1615
devtools::document(pkg = pkg, roclets = roclets, quiet = FALSE)
1716
}

R/track_rust_source.R

Lines changed: 43 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -147,27 +147,11 @@ needs_compilation <- function(path = ".", quiet = FALSE) {
147147
return(TRUE)
148148
}
149149

150-
rust_files <- get_rust_files(path)
151-
152-
# Shortcut: no rust sources found, nothing to track.
153-
if (length(rust_files) == 0L) {
154-
return(FALSE)
155-
}
156-
157-
# Obtains detailed info of each source file and library file.
158-
# This includes 'last modification time'.
159-
# Stored as tibbles
160-
rust_info <- get_file_info(rust_files)
161-
library_info <- get_file_info(library_path)
162-
163150
# Leaves files that were modified *after* the library
164-
modified_files_info <- dplyr::filter(
165-
rust_info,
166-
.data$mtime > library_info[["mtime"]][1]
167-
)
151+
modified_files_paths <- find_newer_files_than(get_rust_files(path), library_path)
168152

169153
# Shortcut: no files have been modified since last compilation.
170-
if (nrow(modified_files_info) == 0L) {
154+
if (length(modified_files_paths) == 0L) {
171155
return(FALSE)
172156
}
173157

@@ -177,7 +161,7 @@ needs_compilation <- function(path = ".", quiet = FALSE) {
177161
# do not support `quiet` arg.
178162
if (!isTRUE(quiet)) {
179163
purrr::walk(
180-
pretty_rel_path(modified_files_info[["path"]], search_from = path),
164+
pretty_rel_path(modified_files_paths, search_from = path),
181165
~ cli::cli_alert_info("File {.file {.x}} has been modified since last compilation.")
182166
)
183167
}
@@ -200,3 +184,43 @@ get_file_info <- function(path) {
200184
info <- dplyr::mutate(info, path = rownames(info), .before = dplyr::everything())
201185
tibble::as_tibble(info)
202186
}
187+
188+
# Find files newer than the reference file.
189+
#
190+
# @param files File paths to find newer ones than `reference`.
191+
# @param reference A file path to compare `files` against.
192+
# @return File paths newer than `reference`.
193+
find_newer_files_than <- function(files, reference) {
194+
# If `files` is of length 0 or NULL, exit early.
195+
if (length(files) == 0L) {
196+
return(character(0))
197+
}
198+
199+
error_details <- character(0)
200+
201+
if (length(reference) != 1L) {
202+
error_details <- ui_x("Expected vector of length `1`, got `{length(reference)}`.")
203+
}
204+
205+
if (typeof(reference) != "character") {
206+
error_details <- c(error_details, ui_x("Expected type `character`, got `{typeof(reference)}`."))
207+
}
208+
209+
# if `reference` is already found invalid, skip checking the existence
210+
if (length(error_details) == 0L && !file.exists(reference)) {
211+
error_details <- ui_x("File {reference} doesn't exist.")
212+
}
213+
214+
if (length(error_details) > 0L) {
215+
ui_throw("Invalid argument `reference`.", details = error_details)
216+
}
217+
218+
reference_mtime <- get_file_info(reference)[["mtime"]]
219+
220+
modified_files_info <- dplyr::filter(
221+
get_file_info(files),
222+
.data$mtime > reference_mtime
223+
)
224+
225+
modified_files_info[["path"]]
226+
}

man/register_extendr.Rd

Lines changed: 10 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-helper-functions.R

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,26 @@ test_that("`get_file_info()` and `file.info()` outputs are equivalent", {
144144
expect_true(is.na(na_info[["ctime"]]))
145145
expect_true(is.na(na_info[["atime"]]))
146146
})
147+
148+
test_that("find_newer_files_than() works", {
149+
old_file <- tempfile()
150+
new_file1 <- tempfile()
151+
new_file2 <- tempfile()
152+
brio::write_file("", old_file)
153+
Sys.sleep(0.01)
154+
brio::write_file("", new_file1)
155+
brio::write_file("", new_file2)
156+
157+
# files are older than reference
158+
expect_equal(find_newer_files_than(old_file, new_file1), character(0))
159+
# files are newer than reference
160+
expect_equal(find_newer_files_than(new_file1, old_file), new_file1)
161+
# multiple files
162+
expect_equal(find_newer_files_than(c(new_file1, new_file2), old_file), c(new_file1, new_file2))
163+
# no files
164+
expect_equal(find_newer_files_than(character(0), old_file), character(0))
165+
expect_equal(find_newer_files_than(NA_character_, old_file), character(0))
166+
# invalid cases
167+
expect_error(find_newer_files_than(old_file, character(0)))
168+
expect_error(find_newer_files_than(old_file, "/no/such/files"))
169+
})

0 commit comments

Comments
 (0)