From 76e16424fdd551d5629e0c125574b3f02fd82296 Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 11 Oct 2025 23:57:19 +0300 Subject: [PATCH 01/14] Prototype use_rtools --- R/use_rtools.R | 114 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 R/use_rtools.R diff --git a/R/use_rtools.R b/R/use_rtools.R new file mode 100644 index 00000000..dcde1878 --- /dev/null +++ b/R/use_rtools.R @@ -0,0 +1,114 @@ +is_windows_arm <- function() { + proc_arch <- Sys.getenv("PROCESSOR_ARCHITECTURE") + r_arch <- R.version[["arch"]] + + if (identical(proc_arch, "ARM64") && !identical(r_arch, "aarch64")) { + cli::cli_abort( + c( + "Architecture mismatch detected.", + "i" = "You are running the {.code {proc_arch}} version of Windows, but the {.code {r_arch}} version of R.", + "i" = "You can find ARM64 build of R at {.url https://www.r-project.org/nosvn/winutf8/aarch64}" + ), + class = "rextendr_error" + ) + } + + identical(proc_arch, "ARM64") && identical(r_arch, "aarch64") +} + +throw_if_no_rtools <- function() { + if (!suppressMessages(pkgbuild::has_rtools())) { + cli::cli_abort( + c( + "Unable to find Rtools that are needed for compilation.", + "i" = "Required version is {.emph {pkgbuild::rtools_needed()}}." + ), + class = "rextendr_error" + ) + } +} + +throw_if_not_ucrt <- function() { + if (!identical(R.version$crt, "ucrt")) { + cli::cli_abort( + c( + "R must be built with UCRT to use rextendr.", + "i" = "Please install the UCRT version of R from {.url https://cran.r-project.org/}." + ), + class = "rextendr_error" + ) + } +} + +get_rtools_version <- function() { + minor_patch <- package_version(R.version$minor) + + if (minor_patch >= "5.0") { + "45" + } else if (minor_patch >= "4.0") { + "44" + } else if (minor_patch >= "3.0") { + "43" + } else { + "42" + } +} + +get_path_to_cargo_folder_arm <- function(rtools_root) { + path_to_cargo_folder <- file.path(rtools_root, "clangarm64", "bin") + path_to_cargo <- file.path(path_to_cargo_folder, "cargo.exe") + if (!file.exists(path_to_cargo)) { + cli::cli_abort( + c( + "{.code rextendr} on ARM Windows requires an ARM-compatible Rust toolchain.", + "i" = "Check this instructions to set up {.code cargo} using ARM version of RTools: {.url https://github.com/r-rust/faq?tab=readme-ov-file#does-rust-support-windows-on-arm64-aarch64}." + ), + class = "rextendr_error" + ) + } + + normalizePath(path_to_cargo_folder, mustWork = TRUE) +} + +use_rtools <- function(.local_envir = parent.frame()) { + throw_if_no_rtools() + throw_if_not_ucrt() + + minor_patch <- package_version(R.version$minor) + + is_arm <- is_windows_arm() + + rtools_version <- get_rtools_version() + + env_var <- if (is_arm) { + sprintf("RTOOLS%s_AARCH64_HOME", rtools_version) + } else { + sprintf("RTOOLS%s_HOME", rtools_version) + } + + default_path <- if (is_arm) { + sprintf("C:\\rtools%s-aarch64", rtools_version) + } else { + sprintf("C:\\rtools%s", rtools_version) + } + + rtools_home <- normalizePath( + Sys.getenv(env_var, default_path), + mustWork = TRUE + ) + + # c.f. https://github.com/wch/r-source/blob/f09d3d7fa4af446ad59a375d914a0daf3ffc4372/src/library/profile/Rprofile.windows#L70-L71 # nolint: line_length_linter + subdir <- if (is_arm) { + c("aarch64-w64-mingw32.static.posix", "usr") + } else { + c("x86_64-w64-mingw32.static.posix", "usr") + } + + rtools_bin_path <- normalizePath(file.path(rtools_home, subdir, "bin")) + withr::local_path(rtools_bin_path, action = "suffix", .local_envir = .local_envir) + + if(is_arm){ + cargo_path <- get_path_to_cargo_folder_arm(rtools_home) + withr::local_path(cargo_path, .local_envir = .local_envir) + } +} From 5b8eaa2a6b0adccc91701ea6f5a575f06ae5e0e0 Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 11 Oct 2025 23:59:14 +0300 Subject: [PATCH 02/14] Update RTools usage and target --- R/source.R | 64 +++++------------------------------------------------- 1 file changed, 5 insertions(+), 59 deletions(-) diff --git a/R/source.R b/R/source.R index 2aa5ca5d..66487e9e 100644 --- a/R/source.R +++ b/R/source.R @@ -278,65 +278,7 @@ rust_source <- function( # append rtools path to the end of PATH on Windows if (opts[["use_rtools"]] && .Platform$OS.type == "windows") { - if (!suppressMessages(pkgbuild::has_rtools())) { - cli::cli_abort( - c( - "Unable to find Rtools that are needed for compilation.", - "i" = "Required version is {.emph {pkgbuild::rtools_needed()}}." - ), - class = "rextendr_error" - ) - } - - if (identical(R.version$crt, "ucrt")) { - # TODO: update this when R 5.0 is released. - if (!identical(R.version$major, "4")) { - cli::cli_abort( - "rextendr currently supports R 4.2", - call = rlang::caller_call(), - class = "rextendr_error" - ) - } - - minor_patch <- package_version(R.version$minor) - - if (minor_patch >= "5.0") { - rtools_version <- "45" # nolint: object_usage_linter - } else if (minor_patch >= "4.0") { - rtools_version <- "44" # nolint: object_usage_linter - } else if (minor_patch >= "3.0") { - rtools_version <- "43" # nolint: object_usage_linter - } else { - rtools_version <- "42" # nolint: object_usage_linter - } - - rtools_home <- normalizePath( - Sys.getenv( - glue("RTOOLS{rtools_version}_HOME"), - glue("C:\\rtools{rtools_version}") - ), - mustWork = TRUE - ) - - # c.f. https://github.com/wch/r-source/blob/f09d3d7fa4af446ad59a375d914a0daf3ffc4372/src/library/profile/Rprofile.windows#L70-L71 # nolint: line_length_linter - subdir <- c("x86_64-w64-mingw32.static.posix", "usr") - } else { - # rtools_path() returns path to the RTOOLS40_HOME\usr\bin, - # but we need RTOOLS40_HOME\mingw{arch}\bin, hence the "../.." - rtools_home <- normalizePath( - # `pkgbuild` may return two paths for R < 4.2 with Rtools40v2 - file.path(pkgbuild::rtools_path()[1], "..", ".."), - winslash = "/", - mustWork = TRUE - ) - - subdir <- paste0("mingw", ifelse(R.version$arch == "i386", "32", "64")) - # if RTOOLS40_HOME is properly set, this will have no real effect - withr::local_envvar(RTOOLS40_HOME = rtools_home) - } - - rtools_bin_path <- normalizePath(file.path(rtools_home, subdir, "bin")) - withr::local_path(rtools_bin_path, action = "suffix") + use_rtools() } # get target name, not null for Windows @@ -804,6 +746,10 @@ get_specific_target_name <- function() { return("i686-pc-windows-gnu") } + if (R.version$arch == "aarch64") { + return(NULL) + } + cli::cli_abort( "Unknown Windows architecture", class = "rextendr_error" From 67891d13cbc11c8a2f69fe782953309ab8401d5a Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sun, 12 Oct 2025 00:39:46 +0300 Subject: [PATCH 03/14] Lint --- R/use_rtools.R | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/R/use_rtools.R b/R/use_rtools.R index dcde1878..0667cc52 100644 --- a/R/use_rtools.R +++ b/R/use_rtools.R @@ -61,7 +61,7 @@ get_path_to_cargo_folder_arm <- function(rtools_root) { cli::cli_abort( c( "{.code rextendr} on ARM Windows requires an ARM-compatible Rust toolchain.", - "i" = "Check this instructions to set up {.code cargo} using ARM version of RTools: {.url https://github.com/r-rust/faq?tab=readme-ov-file#does-rust-support-windows-on-arm64-aarch64}." + "i" = "Check this instructions to set up {.code cargo} using ARM version of RTools: {.url https://github.com/r-rust/faq?tab=readme-ov-file#does-rust-support-windows-on-arm64-aarch64}." # nolint: line_length_linter ), class = "rextendr_error" ) @@ -74,8 +74,6 @@ use_rtools <- function(.local_envir = parent.frame()) { throw_if_no_rtools() throw_if_not_ucrt() - minor_patch <- package_version(R.version$minor) - is_arm <- is_windows_arm() rtools_version <- get_rtools_version() @@ -107,7 +105,7 @@ use_rtools <- function(.local_envir = parent.frame()) { rtools_bin_path <- normalizePath(file.path(rtools_home, subdir, "bin")) withr::local_path(rtools_bin_path, action = "suffix", .local_envir = .local_envir) - if(is_arm){ + if (is_arm) { cargo_path <- get_path_to_cargo_folder_arm(rtools_home) withr::local_path(cargo_path, .local_envir = .local_envir) } From a88afb4cb6d2ee8f1ff6a1ed47785de3fccdde1a Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 18 Oct 2025 15:53:27 +0300 Subject: [PATCH 04/14] Initial draft for tests --- DESCRIPTION | 2 + R/use_rtools.R | 10 +- tests/testthat/test-use_rtools.R | 159 +++++++++++++++++++++++++++++++ 3 files changed, 168 insertions(+), 3 deletions(-) create mode 100644 tests/testthat/test-use_rtools.R diff --git a/DESCRIPTION b/DESCRIPTION index 1f8ef22f..cf468883 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -69,6 +69,8 @@ Suggests: rcmdcheck, knitr, lintr, + mockery (>= 0.4.5), + patrick (>= 0.3.0), rmarkdown, testthat (>= 3.1.7), usethis diff --git a/R/use_rtools.R b/R/use_rtools.R index 0667cc52..3f563a86 100644 --- a/R/use_rtools.R +++ b/R/use_rtools.R @@ -1,6 +1,10 @@ +get_r_version <- function () { + R.version +} + is_windows_arm <- function() { proc_arch <- Sys.getenv("PROCESSOR_ARCHITECTURE") - r_arch <- R.version[["arch"]] + r_arch <- get_r_version()[["arch"]] if (identical(proc_arch, "ARM64") && !identical(r_arch, "aarch64")) { cli::cli_abort( @@ -29,7 +33,7 @@ throw_if_no_rtools <- function() { } throw_if_not_ucrt <- function() { - if (!identical(R.version$crt, "ucrt")) { + if (!identical(get_r_version()[["ucrt"]], "ucrt")) { cli::cli_abort( c( "R must be built with UCRT to use rextendr.", @@ -41,7 +45,7 @@ throw_if_not_ucrt <- function() { } get_rtools_version <- function() { - minor_patch <- package_version(R.version$minor) + minor_patch <- package_version(get_r_version()[["minor"]]) if (minor_patch >= "5.0") { "45" diff --git a/tests/testthat/test-use_rtools.R b/tests/testthat/test-use_rtools.R new file mode 100644 index 00000000..581b4c69 --- /dev/null +++ b/tests/testthat/test-use_rtools.R @@ -0,0 +1,159 @@ +test_that("is_windows_arm returns TRUE on Windows ARM64 with R aarch64", { + getenv_mock <- mockery::mock("ARM64") + abort_spy <- mockery::mock() + + mockery::stub(is_windows_arm, "Sys.getenv", getenv_mock) + mockery::stub(is_windows_arm, "cli::cli_abort", abort_spy) + mockery::stub(is_windows_arm, "get_r_version", list(arch = "aarch64")) + + result <- is_windows_arm() + + expect_true(result) + + mockery::expect_called(getenv_mock, 1) + mockery::expect_args(getenv_mock, 1, "PROCESSOR_ARCHITECTURE") + + mockery::expect_called(abort_spy, 0) +}) + +test_that("is_windows_arm returns FALSE on Windows AMD64 with R x86_64", { + getenv_mock <- mockery::mock("AMD64") + abort_spy <- mockery::mock() + + mockery::stub(is_windows_arm, "Sys.getenv", getenv_mock) + mockery::stub(is_windows_arm, "cli::cli_abort", abort_spy) + mockery::stub(is_windows_arm, "get_r_version", list(arch = "x86_64")) + + result <- is_windows_arm() + + expect_false(result) + + mockery::expect_called(getenv_mock, 1) + mockery::expect_args(getenv_mock, 1, "PROCESSOR_ARCHITECTURE") + + mockery::expect_called(abort_spy, 0) +}) + +test_that("is_windows_arm throws on Windows ARM64 with R not aarch64", { + getenv_mock <- mockery::mock("ARM64") + abort_mock <- mockery::mock(stop("Aborted in test")) + + mockery::stub(is_windows_arm, "Sys.getenv", getenv_mock) + mockery::stub(is_windows_arm, "cli::cli_abort", abort_mock) + mockery::stub(is_windows_arm, "get_r_version", list(arch = "x64")) + + expect_error(is_windows_arm(), "Aborted in test") + + abort_mock_args <- mockery::mock_args(abort_mock)[[1]] + expect_equal(abort_mock_args[[1]][[1]], "Architecture mismatch detected.") + expect_equal(abort_mock_args[["class"]], "rextendr_error") +}) + +test_that("throw_if_no_rtools throws when Rtools is not found", { + abort_mock <- mockery::mock(stop("Aborted in test")) + + mockery::stub(throw_if_no_rtools, "pkgbuild::has_rtools", FALSE) + mockery::stub(throw_if_no_rtools, "cli::cli_abort", abort_mock) + + expect_error(throw_if_no_rtools(), "Aborted in test") + + abort_mock_args <- mockery::mock_args(abort_mock)[[1]] + expect_equal(abort_mock_args[[1]][[1]], "Unable to find Rtools that are needed for compilation.") + expect_equal(abort_mock_args[["class"]], "rextendr_error") +}) + +test_that("throw_if_no_rtools does not throw when Rtools is found", { + has_rtools_mock <- mockery::mock(TRUE) + abort_mock <- mockery::mock(stop("Aborted in test")) + + mockery::stub(throw_if_no_rtools, "pkgbuild::has_rtools", TRUE) + mockery::stub(throw_if_no_rtools, "cli::cli_abort", abort_mock) + + expect_silent(throw_if_no_rtools()) + + mockery::expect_called(abort_mock, 0) +}) + +test_that("throw_if_not_ucrt throws when R is not UCRT", { + abort_mock <- mockery::mock(stop("Aborted in test")) + + mockery::stub(throw_if_not_ucrt, "get_r_version", list(ucrt = "non-ucrt")) + mockery::stub(throw_if_not_ucrt, "cli::cli_abort", abort_mock) + + expect_error(throw_if_not_ucrt(), "Aborted in test") + + abort_mock_args <- mockery::mock_args(abort_mock)[[1]] + expect_equal(abort_mock_args[[1]][[1]], "R must be built with UCRT to use rextendr.") + expect_equal(abort_mock_args[["class"]], "rextendr_error") +}) + +test_that("throw_if_not_ucrt does not throw when R is UCRT", { + abort_mock <- mockery::mock(stop("Aborted in test")) + + mockery::stub(throw_if_not_ucrt, "get_r_version", list(ucrt = "ucrt")) + mockery::stub(throw_if_not_ucrt, "cli::cli_abort", abort_mock) + + expect_silent(throw_if_not_ucrt()) + + mockery::expect_called(abort_mock, 0) +}) + +patrick::with_parameters_test_that("get_rtools_version returns correct Rtools version:", { + mockery::stub(get_rtools_version, "get_r_version", list(minor = minor_version)) + + result <- get_rtools_version() + + expect_equal(result, expected_rtools_version) +}, + minor_version = c("5.1", "5.0", "4.3", "4.2", "4.1", "4.0", "3.3", "2.3"), + expected_rtools_version = c("45", "45", "44", "44", "44", "44", "43", "42"), + .test_name = "when R minor version is {minor_version}, Rtools should be {expected_rtools_version}" +) + +test_that("get_path_to_cargo_folder_arm constructs correct path when folder exists", { + path_to_cargo_folder_stub <- "path/to/cargo/folder" + path_to_cargo_stub <- "path/to/cargo" + normalized_path <- "normalized/path" + + abort_spy <- mockery::mock() + file_path_mock <- mockery::mock(path_to_cargo_folder_stub, path_to_cargo_stub) + normalize_path_mock <- mockery::mock(normalized_path) + file_exists_mock <- mockery::mock(TRUE) + + mockery::stub(get_path_to_cargo_folder_arm, "file.path", file_path_mock) + mockery::stub(get_path_to_cargo_folder_arm, "file.exists", file_exists_mock) + mockery::stub(get_path_to_cargo_folder_arm, "cli::cli_abort", abort_spy) + mockery::stub(get_path_to_cargo_folder_arm, "normalizePath", normalize_path_mock) + + result <- get_path_to_cargo_folder_arm("rtools/root") + + expect_equal(result, normalized_path) + mockery::expect_args(file_path_mock, 1, "rtools/root", "clangarm64", "bin") + mockery::expect_args(file_path_mock, 2, path_to_cargo_folder_stub, "cargo.exe") + mockery::expect_args(file_exists_mock, 1, path_to_cargo_stub) + mockery::expect_args(normalize_path_mock, 1, path_to_cargo_folder_stub, mustWork = TRUE) + mockery::expect_called(abort_spy, 0) +}) + +test_that("get_path_to_cargo_folder_arm throws when cargo.exe does not exist", { + path_to_cargo_folder_stub <- "path/to/cargo/folder" + path_to_cargo_stub <- "path/to/cargo" + + abort_mock <- mockery::mock(stop("Aborted in test")) + file_path_mock <- mockery::mock(path_to_cargo_folder_stub, path_to_cargo_stub) + file_exists_mock <- mockery::mock(FALSE) + + mockery::stub(get_path_to_cargo_folder_arm, "file.path", file_path_mock) + mockery::stub(get_path_to_cargo_folder_arm, "file.exists", file_exists_mock) + mockery::stub(get_path_to_cargo_folder_arm, "cli::cli_abort", abort_mock) + + expect_error(get_path_to_cargo_folder_arm("rtools/root"), "Aborted in test") + + mockery::expect_args(file_path_mock, 1, "rtools/root", "clangarm64", "bin") + mockery::expect_args(file_path_mock, 2, path_to_cargo_folder_stub, "cargo.exe") + mockery::expect_args(file_exists_mock, 1, path_to_cargo_stub) + + abort_mock_args <- mockery::mock_args(abort_mock)[[1]] + expect_equal(abort_mock_args[[1]][[1]], "{.code rextendr} on ARM Windows requires an ARM-compatible Rust toolchain.") + expect_equal(abort_mock_args[["class"]], "rextendr_error") +}) \ No newline at end of file From 7bedc2e9bd0333b43ff9ce5afadd22b3426d2c91 Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 18 Oct 2025 16:08:00 +0300 Subject: [PATCH 05/14] Checkpoint --- R/use_rtools.R | 4 +- tests/testthat/test-use_rtools.R | 85 ++++++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 1 deletion(-) diff --git a/R/use_rtools.R b/R/use_rtools.R index 3f563a86..0cad6ebf 100644 --- a/R/use_rtools.R +++ b/R/use_rtools.R @@ -106,11 +106,13 @@ use_rtools <- function(.local_envir = parent.frame()) { c("x86_64-w64-mingw32.static.posix", "usr") } - rtools_bin_path <- normalizePath(file.path(rtools_home, subdir, "bin")) + rtools_bin_path <- normalizePath(file.path(rtools_home, subdir, "bin"), mustWork = TRUE) withr::local_path(rtools_bin_path, action = "suffix", .local_envir = .local_envir) if (is_arm) { cargo_path <- get_path_to_cargo_folder_arm(rtools_home) withr::local_path(cargo_path, .local_envir = .local_envir) } + + invisible() } diff --git a/tests/testthat/test-use_rtools.R b/tests/testthat/test-use_rtools.R index 581b4c69..5cac4e7c 100644 --- a/tests/testthat/test-use_rtools.R +++ b/tests/testthat/test-use_rtools.R @@ -156,4 +156,89 @@ test_that("get_path_to_cargo_folder_arm throws when cargo.exe does not exist", { abort_mock_args <- mockery::mock_args(abort_mock)[[1]] expect_equal(abort_mock_args[[1]][[1]], "{.code rextendr} on ARM Windows requires an ARM-compatible Rust toolchain.") expect_equal(abort_mock_args[["class"]], "rextendr_error") +}) + +test_that("use_rtools handled x86_64 architecture", { + rtools_version <- "rtools_version" + env_var <- "env_var" + default_path <- "default_path" + rtools_home <- "rtools_home" + sys_getenv_result <- "sys_getenv_result" + file_path_result <- "file_path_result" + rtools_bin_path <- "rtools_bin_path" + + sprintf_mock <- mockery::mock(env_var, default_path) + normalize_path_mock <- mockery::mock(rtools_home, rtools_bin_path) + sys_getenv_mock <- mockery::mock(sys_getenv_result) + file_path_mock <- mockery::mock(file_path_result) + withr_local_path_mock <- mockery::mock() + get_path_to_cargo_folder_arm_mock <- mockery::mock() + + mockery::stub(use_rtools, "throw_if_no_rtools", NULL) + mockery::stub(use_rtools, "throw_if_not_ucrt", NULL) + mockery::stub(use_rtools, "is_windows_arm", FALSE) + mockery::stub(use_rtools, "get_rtools_version", rtools_version) + + mockery::stub(use_rtools, "sprintf", sprintf_mock) + mockery::stub(use_rtools, "Sys.getenv", sys_getenv_mock) + mockery::stub(use_rtools, "normalizePath", normalize_path_mock) + mockery::stub(use_rtools, "file.path", file_path_mock) + mockery::stub(use_rtools, "withr::local_path", withr_local_path_mock) + mockery::stub(use_rtools, "get_path_to_cargo_folder_arm", get_path_to_cargo_folder_arm_mock) + + parent_env <- "parent_env" + + use_rtools(parent_env) + + mockery::expect_args(sprintf_mock, 1, "RTOOLS%s_HOME", rtools_version) + mockery::expect_args(sprintf_mock, 2, "C:\\rtools%s", rtools_version) + mockery::expect_args(sys_getenv_mock, 1, env_var, default_path) + mockery::expect_args(normalize_path_mock, 1, sys_getenv_result, mustWork = TRUE) + mockery::expect_args(normalize_path_mock, 2, file_path_result, mustWork = TRUE) + mockery::expect_args(file_path_mock, 1, rtools_home, c("x86_64-w64-mingw32.static.posix", "usr"), "bin") + mockery::expect_args(withr_local_path_mock, 1, rtools_bin_path, action = "suffix", .local_envir = parent_env) +}) + +test_that("use_rtools handled ARM64 architecture", { + rtools_version <- "rtools_version" + env_var <- "env_var" + default_path <- "default_path" + rtools_home <- "rtools_home" + sys_getenv_result <- "sys_getenv_result" + file_path_result <- "file_path_result" + rtools_bin_path <- "rtools_bin_path" + cargo_path <- "cargo_path" + + sprintf_mock <- mockery::mock(env_var, default_path) + normalize_path_mock <- mockery::mock(rtools_home, rtools_bin_path) + sys_getenv_mock <- mockery::mock(sys_getenv_result) + file_path_mock <- mockery::mock(file_path_result) + withr_local_path_mock <- mockery::mock() + get_path_to_cargo_folder_arm_mock <- mockery::mock(cargo_path) + + mockery::stub(use_rtools, "throw_if_no_rtools", NULL) + mockery::stub(use_rtools, "throw_if_not_ucrt", NULL) + mockery::stub(use_rtools, "is_windows_arm", TRUE) + mockery::stub(use_rtools, "get_rtools_version", rtools_version) + + mockery::stub(use_rtools, "sprintf", sprintf_mock) + mockery::stub(use_rtools, "Sys.getenv", sys_getenv_mock) + mockery::stub(use_rtools, "normalizePath", normalize_path_mock) + mockery::stub(use_rtools, "file.path", file_path_mock) + mockery::stub(use_rtools, "withr::local_path", withr_local_path_mock) + mockery::stub(use_rtools, "get_path_to_cargo_folder_arm", get_path_to_cargo_folder_arm_mock) + + parent_env <- "parent_env" + + use_rtools(parent_env) + + mockery::expect_args(sprintf_mock, 1, "RTOOLS%s_AARCH64_HOME", rtools_version) + mockery::expect_args(sprintf_mock, 2, "C:\\rtools%s-aarch64", rtools_version) + mockery::expect_args(sys_getenv_mock, 1, env_var, default_path) + mockery::expect_args(normalize_path_mock, 1, sys_getenv_result, mustWork = TRUE) + mockery::expect_args(normalize_path_mock, 2, file_path_result, mustWork = TRUE) + mockery::expect_args(file_path_mock, 1, rtools_home, c("aarch64-w64-mingw32.static.posix", "usr"), "bin") + mockery::expect_args(withr_local_path_mock, 1, rtools_bin_path, action = "suffix", .local_envir = parent_env) + mockery::expect_args(get_path_to_cargo_folder_arm_mock, 1, rtools_home) + mockery::expect_args(withr_local_path_mock, 2, cargo_path, .local_envir = parent_env) }) \ No newline at end of file From 563ac032156eb87e4cbb1603f8e0c58c8d0228cb Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 18 Oct 2025 16:14:45 +0300 Subject: [PATCH 06/14] Refactor more --- R/use_rtools.R | 30 ++++++++++------- tests/testthat/test-use_rtools.R | 56 +++++++++----------------------- 2 files changed, 34 insertions(+), 52 deletions(-) diff --git a/R/use_rtools.R b/R/use_rtools.R index 0cad6ebf..edc31a1a 100644 --- a/R/use_rtools.R +++ b/R/use_rtools.R @@ -74,14 +74,7 @@ get_path_to_cargo_folder_arm <- function(rtools_root) { normalizePath(path_to_cargo_folder, mustWork = TRUE) } -use_rtools <- function(.local_envir = parent.frame()) { - throw_if_no_rtools() - throw_if_not_ucrt() - - is_arm <- is_windows_arm() - - rtools_version <- get_rtools_version() - +get_rtools_home <- function(rtools_version, is_arm) { env_var <- if (is_arm) { sprintf("RTOOLS%s_AARCH64_HOME", rtools_version) } else { @@ -94,19 +87,32 @@ use_rtools <- function(.local_envir = parent.frame()) { sprintf("C:\\rtools%s", rtools_version) } - rtools_home <- normalizePath( + normalizePath( Sys.getenv(env_var, default_path), mustWork = TRUE ) +} +get_rtools_bin_path <- function(rtools_home, is_arm) { # c.f. https://github.com/wch/r-source/blob/f09d3d7fa4af446ad59a375d914a0daf3ffc4372/src/library/profile/Rprofile.windows#L70-L71 # nolint: line_length_linter subdir <- if (is_arm) { - c("aarch64-w64-mingw32.static.posix", "usr") + c("aarch64-w64-mingw32.static.posix", "usr", "bin") } else { - c("x86_64-w64-mingw32.static.posix", "usr") + c("x86_64-w64-mingw32.static.posix", "usr", "bin") } - rtools_bin_path <- normalizePath(file.path(rtools_home, subdir, "bin"), mustWork = TRUE) + normalizePath(file.path(rtools_home, subdir), mustWork = TRUE) +} + +use_rtools <- function(.local_envir = parent.frame()) { + throw_if_no_rtools() + throw_if_not_ucrt() + + is_arm <- is_windows_arm() + rtools_version <- get_rtools_version() + rtools_home <- get_rtools_home(rtools_version, is_arm) + rtools_bin_path <- get_rtools_bin_path(rtools_home, is_arm) + withr::local_path(rtools_bin_path, action = "suffix", .local_envir = .local_envir) if (is_arm) { diff --git a/tests/testthat/test-use_rtools.R b/tests/testthat/test-use_rtools.R index 5cac4e7c..9f72fcae 100644 --- a/tests/testthat/test-use_rtools.R +++ b/tests/testthat/test-use_rtools.R @@ -160,60 +160,41 @@ test_that("get_path_to_cargo_folder_arm throws when cargo.exe does not exist", { test_that("use_rtools handled x86_64 architecture", { rtools_version <- "rtools_version" - env_var <- "env_var" - default_path <- "default_path" rtools_home <- "rtools_home" - sys_getenv_result <- "sys_getenv_result" - file_path_result <- "file_path_result" rtools_bin_path <- "rtools_bin_path" - sprintf_mock <- mockery::mock(env_var, default_path) - normalize_path_mock <- mockery::mock(rtools_home, rtools_bin_path) - sys_getenv_mock <- mockery::mock(sys_getenv_result) - file_path_mock <- mockery::mock(file_path_result) withr_local_path_mock <- mockery::mock() - get_path_to_cargo_folder_arm_mock <- mockery::mock() + get_rtools_home_mock <- mockery::mock(rtools_home) + get_rtools_bin_path_mock <- mockery::mock(rtools_bin_path) mockery::stub(use_rtools, "throw_if_no_rtools", NULL) mockery::stub(use_rtools, "throw_if_not_ucrt", NULL) mockery::stub(use_rtools, "is_windows_arm", FALSE) mockery::stub(use_rtools, "get_rtools_version", rtools_version) - mockery::stub(use_rtools, "sprintf", sprintf_mock) - mockery::stub(use_rtools, "Sys.getenv", sys_getenv_mock) - mockery::stub(use_rtools, "normalizePath", normalize_path_mock) - mockery::stub(use_rtools, "file.path", file_path_mock) mockery::stub(use_rtools, "withr::local_path", withr_local_path_mock) - mockery::stub(use_rtools, "get_path_to_cargo_folder_arm", get_path_to_cargo_folder_arm_mock) + mockery::stub(use_rtools, "get_path_to_cargo_folder_arm", function(...) stop("Should not be called")) + mockery::stub(use_rtools, "get_rtools_home", get_rtools_home_mock) + mockery::stub(use_rtools, "get_rtools_bin_path", get_rtools_bin_path_mock) parent_env <- "parent_env" use_rtools(parent_env) - mockery::expect_args(sprintf_mock, 1, "RTOOLS%s_HOME", rtools_version) - mockery::expect_args(sprintf_mock, 2, "C:\\rtools%s", rtools_version) - mockery::expect_args(sys_getenv_mock, 1, env_var, default_path) - mockery::expect_args(normalize_path_mock, 1, sys_getenv_result, mustWork = TRUE) - mockery::expect_args(normalize_path_mock, 2, file_path_result, mustWork = TRUE) - mockery::expect_args(file_path_mock, 1, rtools_home, c("x86_64-w64-mingw32.static.posix", "usr"), "bin") + mockery::expect_args(get_rtools_home_mock, 1, rtools_version, FALSE) + mockery::expect_args(get_rtools_bin_path_mock, 1, rtools_home, FALSE) mockery::expect_args(withr_local_path_mock, 1, rtools_bin_path, action = "suffix", .local_envir = parent_env) }) -test_that("use_rtools handled ARM64 architecture", { +test_that("use_rtools handled aarch64 architecture", { rtools_version <- "rtools_version" - env_var <- "env_var" - default_path <- "default_path" rtools_home <- "rtools_home" - sys_getenv_result <- "sys_getenv_result" - file_path_result <- "file_path_result" rtools_bin_path <- "rtools_bin_path" cargo_path <- "cargo_path" - sprintf_mock <- mockery::mock(env_var, default_path) - normalize_path_mock <- mockery::mock(rtools_home, rtools_bin_path) - sys_getenv_mock <- mockery::mock(sys_getenv_result) - file_path_mock <- mockery::mock(file_path_result) withr_local_path_mock <- mockery::mock() + get_rtools_home_mock <- mockery::mock(rtools_home) + get_rtools_bin_path_mock <- mockery::mock(rtools_bin_path) get_path_to_cargo_folder_arm_mock <- mockery::mock(cargo_path) mockery::stub(use_rtools, "throw_if_no_rtools", NULL) @@ -221,24 +202,19 @@ test_that("use_rtools handled ARM64 architecture", { mockery::stub(use_rtools, "is_windows_arm", TRUE) mockery::stub(use_rtools, "get_rtools_version", rtools_version) - mockery::stub(use_rtools, "sprintf", sprintf_mock) - mockery::stub(use_rtools, "Sys.getenv", sys_getenv_mock) - mockery::stub(use_rtools, "normalizePath", normalize_path_mock) - mockery::stub(use_rtools, "file.path", file_path_mock) mockery::stub(use_rtools, "withr::local_path", withr_local_path_mock) + mockery::stub(use_rtools, "get_rtools_home", get_rtools_home_mock) + mockery::stub(use_rtools, "get_rtools_bin_path", get_rtools_bin_path_mock) mockery::stub(use_rtools, "get_path_to_cargo_folder_arm", get_path_to_cargo_folder_arm_mock) parent_env <- "parent_env" use_rtools(parent_env) - mockery::expect_args(sprintf_mock, 1, "RTOOLS%s_AARCH64_HOME", rtools_version) - mockery::expect_args(sprintf_mock, 2, "C:\\rtools%s-aarch64", rtools_version) - mockery::expect_args(sys_getenv_mock, 1, env_var, default_path) - mockery::expect_args(normalize_path_mock, 1, sys_getenv_result, mustWork = TRUE) - mockery::expect_args(normalize_path_mock, 2, file_path_result, mustWork = TRUE) - mockery::expect_args(file_path_mock, 1, rtools_home, c("aarch64-w64-mingw32.static.posix", "usr"), "bin") + mockery::expect_args(get_rtools_home_mock, 1, rtools_version, TRUE) + mockery::expect_args(get_rtools_bin_path_mock, 1, rtools_home, TRUE) mockery::expect_args(withr_local_path_mock, 1, rtools_bin_path, action = "suffix", .local_envir = parent_env) mockery::expect_args(get_path_to_cargo_folder_arm_mock, 1, rtools_home) mockery::expect_args(withr_local_path_mock, 2, cargo_path, .local_envir = parent_env) -}) \ No newline at end of file +}) + From 9a8eb3a61665bc0c7450b8f7d51e2620ab4b873c Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 18 Oct 2025 16:26:42 +0300 Subject: [PATCH 07/14] Finish tests --- tests/testthat/test-use_rtools.R | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/testthat/test-use_rtools.R b/tests/testthat/test-use_rtools.R index 9f72fcae..67efa6ac 100644 --- a/tests/testthat/test-use_rtools.R +++ b/tests/testthat/test-use_rtools.R @@ -158,6 +158,58 @@ test_that("get_path_to_cargo_folder_arm throws when cargo.exe does not exist", { expect_equal(abort_mock_args[["class"]], "rextendr_error") }) +patrick::with_parameters_test_that("get_rtools_home returns correct path:", { + env_var <- "env_var" + default_path <- "default_path" + get_env_result <- "get_env_result" + normalize_path_result <- "normalize_path_result" + + sprintf_mock <- mockery::mock(env_var, default_path) + getenv_mock <- mockery::mock(get_env_result) + normalize_path_mock <- mockery::mock(normalize_path_result) + + mockery::stub(get_rtools_home, "sprintf", sprintf_mock) + mockery::stub(get_rtools_home, "Sys.getenv", getenv_mock) + mockery::stub(get_rtools_home, "normalizePath", normalize_path_mock) + + rtools_version <- "rtools_version" + + result <- get_rtools_home(rtools_version, is_arm) + + expect_equal(result, normalize_path_result) + mockery::expect_args(sprintf_mock, 1, rtools_env_var_template, rtools_version) + mockery::expect_args(sprintf_mock, 2, rtools_default_path_template, rtools_version) + mockery::expect_args(getenv_mock, 1, env_var, default_path) + mockery::expect_args(normalize_path_mock, 1, get_env_result, mustWork = TRUE) +}, + is_arm = c(TRUE, FALSE), + rtools_env_var_template = c("RTOOLS%s_AARCH64_HOME", "RTOOLS%s_HOME"), + rtools_default_path_template = c("C:\\rtools%s-aarch64", "C:\\rtools%s"), + .test_name = "when is_arm is {is_arm}, env var should be {rtools_env_var_template} and default path should start with {rtools_default_path_template}" +) + +patrick::with_parameters_test_that("get_rtools_bin_path returns correct path:", { + rtools_home <- "rtools_home" + file_path_result <- "file/path/result" + expected_path <- "normalized/path" + file_path_mock <- mockery::mock(file_path_result) + normalize_path_mock <- mockery::mock(expected_path) + + mockery::stub(get_rtools_bin_path, "file.path", file_path_mock) + mockery::stub(get_rtools_bin_path, "normalizePath", normalize_path_mock) + + result <- get_rtools_bin_path(rtools_home, is_arm) + + expect_equal(result, expected_path) + expected_arg <- c(subdir, "usr", "bin") + mockery::expect_args(file_path_mock, 1, rtools_home, expected_arg) + mockery::expect_args(normalize_path_mock, 1, file_path_result, mustWork = TRUE) +}, + is_arm = c(TRUE, FALSE), + subdir = c("aarch64-w64-mingw32.static.posix", "x86_64-w64-mingw32.static.posix"), + .test_name = "when is_arm is {is_arm}, subdir should start with {subdir}" +) + test_that("use_rtools handled x86_64 architecture", { rtools_version <- "rtools_version" rtools_home <- "rtools_home" From 3f20e3ce3e04ad9e60ee29d4c1c617300f1c873e Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 18 Oct 2025 16:29:54 +0300 Subject: [PATCH 08/14] Lint --- R/use_rtools.R | 2 +- tests/testthat/test-use_rtools.R | 86 ++++++++++++++++---------------- 2 files changed, 45 insertions(+), 43 deletions(-) diff --git a/R/use_rtools.R b/R/use_rtools.R index edc31a1a..eee4143d 100644 --- a/R/use_rtools.R +++ b/R/use_rtools.R @@ -1,4 +1,4 @@ -get_r_version <- function () { +get_r_version <- function() { R.version } diff --git a/tests/testthat/test-use_rtools.R b/tests/testthat/test-use_rtools.R index 67efa6ac..28211049 100644 --- a/tests/testthat/test-use_rtools.R +++ b/tests/testthat/test-use_rtools.R @@ -98,13 +98,14 @@ test_that("throw_if_not_ucrt does not throw when R is UCRT", { mockery::expect_called(abort_mock, 0) }) -patrick::with_parameters_test_that("get_rtools_version returns correct Rtools version:", { - mockery::stub(get_rtools_version, "get_r_version", list(minor = minor_version)) +patrick::with_parameters_test_that("get_rtools_version returns correct Rtools version:", + { + mockery::stub(get_rtools_version, "get_r_version", list(minor = minor_version)) - result <- get_rtools_version() + result <- get_rtools_version() - expect_equal(result, expected_rtools_version) -}, + expect_equal(result, expected_rtools_version) + }, minor_version = c("5.1", "5.0", "4.3", "4.2", "4.1", "4.0", "3.3", "2.3"), expected_rtools_version = c("45", "45", "44", "44", "44", "44", "43", "42"), .test_name = "when R minor version is {minor_version}, Rtools should be {expected_rtools_version}" @@ -158,53 +159,55 @@ test_that("get_path_to_cargo_folder_arm throws when cargo.exe does not exist", { expect_equal(abort_mock_args[["class"]], "rextendr_error") }) -patrick::with_parameters_test_that("get_rtools_home returns correct path:", { - env_var <- "env_var" - default_path <- "default_path" - get_env_result <- "get_env_result" - normalize_path_result <- "normalize_path_result" +patrick::with_parameters_test_that("get_rtools_home returns correct path:", + { + env_var <- "env_var" + default_path <- "default_path" + get_env_result <- "get_env_result" + normalize_path_result <- "normalize_path_result" - sprintf_mock <- mockery::mock(env_var, default_path) - getenv_mock <- mockery::mock(get_env_result) - normalize_path_mock <- mockery::mock(normalize_path_result) + sprintf_mock <- mockery::mock(env_var, default_path) + getenv_mock <- mockery::mock(get_env_result) + normalize_path_mock <- mockery::mock(normalize_path_result) - mockery::stub(get_rtools_home, "sprintf", sprintf_mock) - mockery::stub(get_rtools_home, "Sys.getenv", getenv_mock) - mockery::stub(get_rtools_home, "normalizePath", normalize_path_mock) + mockery::stub(get_rtools_home, "sprintf", sprintf_mock) + mockery::stub(get_rtools_home, "Sys.getenv", getenv_mock) + mockery::stub(get_rtools_home, "normalizePath", normalize_path_mock) - rtools_version <- "rtools_version" + rtools_version <- "rtools_version" - result <- get_rtools_home(rtools_version, is_arm) + result <- get_rtools_home(rtools_version, is_arm) - expect_equal(result, normalize_path_result) - mockery::expect_args(sprintf_mock, 1, rtools_env_var_template, rtools_version) - mockery::expect_args(sprintf_mock, 2, rtools_default_path_template, rtools_version) - mockery::expect_args(getenv_mock, 1, env_var, default_path) - mockery::expect_args(normalize_path_mock, 1, get_env_result, mustWork = TRUE) -}, + expect_equal(result, normalize_path_result) + mockery::expect_args(sprintf_mock, 1, rtools_env_var_template, rtools_version) + mockery::expect_args(sprintf_mock, 2, rtools_default_path_template, rtools_version) + mockery::expect_args(getenv_mock, 1, env_var, default_path) + mockery::expect_args(normalize_path_mock, 1, get_env_result, mustWork = TRUE) + }, is_arm = c(TRUE, FALSE), rtools_env_var_template = c("RTOOLS%s_AARCH64_HOME", "RTOOLS%s_HOME"), rtools_default_path_template = c("C:\\rtools%s-aarch64", "C:\\rtools%s"), - .test_name = "when is_arm is {is_arm}, env var should be {rtools_env_var_template} and default path should start with {rtools_default_path_template}" + .test_name = "when is_arm is {is_arm}, env var should be {rtools_env_var_template} and default path should start with {rtools_default_path_template}" # nolint: line_length_linter ) -patrick::with_parameters_test_that("get_rtools_bin_path returns correct path:", { - rtools_home <- "rtools_home" - file_path_result <- "file/path/result" - expected_path <- "normalized/path" - file_path_mock <- mockery::mock(file_path_result) - normalize_path_mock <- mockery::mock(expected_path) +patrick::with_parameters_test_that("get_rtools_bin_path returns correct path:", + { + rtools_home <- "rtools_home" + file_path_result <- "file/path/result" + expected_path <- "normalized/path" + file_path_mock <- mockery::mock(file_path_result) + normalize_path_mock <- mockery::mock(expected_path) - mockery::stub(get_rtools_bin_path, "file.path", file_path_mock) - mockery::stub(get_rtools_bin_path, "normalizePath", normalize_path_mock) + mockery::stub(get_rtools_bin_path, "file.path", file_path_mock) + mockery::stub(get_rtools_bin_path, "normalizePath", normalize_path_mock) - result <- get_rtools_bin_path(rtools_home, is_arm) + result <- get_rtools_bin_path(rtools_home, is_arm) - expect_equal(result, expected_path) - expected_arg <- c(subdir, "usr", "bin") - mockery::expect_args(file_path_mock, 1, rtools_home, expected_arg) - mockery::expect_args(normalize_path_mock, 1, file_path_result, mustWork = TRUE) -}, + expect_equal(result, expected_path) + expected_arg <- c(subdir, "usr", "bin") + mockery::expect_args(file_path_mock, 1, rtools_home, expected_arg) + mockery::expect_args(normalize_path_mock, 1, file_path_result, mustWork = TRUE) + }, is_arm = c(TRUE, FALSE), subdir = c("aarch64-w64-mingw32.static.posix", "x86_64-w64-mingw32.static.posix"), .test_name = "when is_arm is {is_arm}, subdir should start with {subdir}" @@ -246,8 +249,8 @@ test_that("use_rtools handled aarch64 architecture", { withr_local_path_mock <- mockery::mock() get_rtools_home_mock <- mockery::mock(rtools_home) - get_rtools_bin_path_mock <- mockery::mock(rtools_bin_path) - get_path_to_cargo_folder_arm_mock <- mockery::mock(cargo_path) + get_rtools_bin_path_mock <- mockery::mock(rtools_bin_path) # nolint: object_length_linter + get_path_to_cargo_folder_arm_mock <- mockery::mock(cargo_path) # nolint: object_length_linter mockery::stub(use_rtools, "throw_if_no_rtools", NULL) mockery::stub(use_rtools, "throw_if_not_ucrt", NULL) @@ -269,4 +272,3 @@ test_that("use_rtools handled aarch64 architecture", { mockery::expect_args(get_path_to_cargo_folder_arm_mock, 1, rtools_home) mockery::expect_args(withr_local_path_mock, 2, cargo_path, .local_envir = parent_env) }) - From 3f1460dcb9337f9601eeb404ea75a416968bbaf5 Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 18 Oct 2025 16:32:29 +0300 Subject: [PATCH 09/14] Parameterize test --- tests/testthat/test-use_rtools.R | 48 ++++++++++++-------------------- 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/tests/testthat/test-use_rtools.R b/tests/testthat/test-use_rtools.R index 28211049..5127bd19 100644 --- a/tests/testthat/test-use_rtools.R +++ b/tests/testthat/test-use_rtools.R @@ -1,38 +1,26 @@ -test_that("is_windows_arm returns TRUE on Windows ARM64 with R aarch64", { - getenv_mock <- mockery::mock("ARM64") - abort_spy <- mockery::mock() - - mockery::stub(is_windows_arm, "Sys.getenv", getenv_mock) - mockery::stub(is_windows_arm, "cli::cli_abort", abort_spy) - mockery::stub(is_windows_arm, "get_r_version", list(arch = "aarch64")) - - result <- is_windows_arm() - - expect_true(result) - - mockery::expect_called(getenv_mock, 1) - mockery::expect_args(getenv_mock, 1, "PROCESSOR_ARCHITECTURE") - - mockery::expect_called(abort_spy, 0) -}) +patrick::with_parameters_test_that("is_windows_arm: ", + { + getenv_mock <- mockery::mock(proc_arch) + abort_spy <- mockery::mock() -test_that("is_windows_arm returns FALSE on Windows AMD64 with R x86_64", { - getenv_mock <- mockery::mock("AMD64") - abort_spy <- mockery::mock() + mockery::stub(is_windows_arm, "Sys.getenv", getenv_mock) + mockery::stub(is_windows_arm, "cli::cli_abort", abort_spy) + mockery::stub(is_windows_arm, "get_r_version", list(arch = r_arch)) - mockery::stub(is_windows_arm, "Sys.getenv", getenv_mock) - mockery::stub(is_windows_arm, "cli::cli_abort", abort_spy) - mockery::stub(is_windows_arm, "get_r_version", list(arch = "x86_64")) + result <- is_windows_arm() - result <- is_windows_arm() + expect_equal(result, is_arm) - expect_false(result) + mockery::expect_called(getenv_mock, 1) + mockery::expect_args(getenv_mock, 1, "PROCESSOR_ARCHITECTURE") - mockery::expect_called(getenv_mock, 1) - mockery::expect_args(getenv_mock, 1, "PROCESSOR_ARCHITECTURE") - - mockery::expect_called(abort_spy, 0) -}) + mockery::expect_called(abort_spy, 0) + }, + is_arm = c(TRUE, FALSE), + proc_arch = c("ARM64", "AMD64"), + r_arch = c("aarch64", "x86_64"), + .test_name = "when proc_arch is {proc_arch} and r_arch is {r_arch}, returns {is_arm}" +) test_that("is_windows_arm throws on Windows ARM64 with R not aarch64", { getenv_mock <- mockery::mock("ARM64") From 458e17e9195de6da9f2fe71a2144fd40789d8e12 Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 18 Oct 2025 16:33:30 +0300 Subject: [PATCH 10/14] Rename tests --- tests/testthat/test-use_rtools.R | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/testthat/test-use_rtools.R b/tests/testthat/test-use_rtools.R index 5127bd19..fd09f301 100644 --- a/tests/testthat/test-use_rtools.R +++ b/tests/testthat/test-use_rtools.R @@ -86,7 +86,7 @@ test_that("throw_if_not_ucrt does not throw when R is UCRT", { mockery::expect_called(abort_mock, 0) }) -patrick::with_parameters_test_that("get_rtools_version returns correct Rtools version:", +patrick::with_parameters_test_that("get_rtools_version:", { mockery::stub(get_rtools_version, "get_r_version", list(minor = minor_version)) @@ -96,7 +96,7 @@ patrick::with_parameters_test_that("get_rtools_version returns correct Rtools ve }, minor_version = c("5.1", "5.0", "4.3", "4.2", "4.1", "4.0", "3.3", "2.3"), expected_rtools_version = c("45", "45", "44", "44", "44", "44", "43", "42"), - .test_name = "when R minor version is {minor_version}, Rtools should be {expected_rtools_version}" + .test_name = "when R minor version is {minor_version}, returns {expected_rtools_version}" ) test_that("get_path_to_cargo_folder_arm constructs correct path when folder exists", { @@ -147,7 +147,7 @@ test_that("get_path_to_cargo_folder_arm throws when cargo.exe does not exist", { expect_equal(abort_mock_args[["class"]], "rextendr_error") }) -patrick::with_parameters_test_that("get_rtools_home returns correct path:", +patrick::with_parameters_test_that("get_rtools_home:", { env_var <- "env_var" default_path <- "default_path" @@ -178,7 +178,7 @@ patrick::with_parameters_test_that("get_rtools_home returns correct path:", .test_name = "when is_arm is {is_arm}, env var should be {rtools_env_var_template} and default path should start with {rtools_default_path_template}" # nolint: line_length_linter ) -patrick::with_parameters_test_that("get_rtools_bin_path returns correct path:", +patrick::with_parameters_test_that("get_rtools_bin_path:", { rtools_home <- "rtools_home" file_path_result <- "file/path/result" From 25fcb6455b09a1a0fe427875d16fac572c722d45 Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Sat, 18 Oct 2025 17:14:21 +0300 Subject: [PATCH 11/14] Fix typo --- R/use_rtools.R | 2 +- tests/testthat/test-use_rtools.R | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/R/use_rtools.R b/R/use_rtools.R index eee4143d..384b5479 100644 --- a/R/use_rtools.R +++ b/R/use_rtools.R @@ -33,7 +33,7 @@ throw_if_no_rtools <- function() { } throw_if_not_ucrt <- function() { - if (!identical(get_r_version()[["ucrt"]], "ucrt")) { + if (!identical(get_r_version()[["crt"]], "ucrt")) { cli::cli_abort( c( "R must be built with UCRT to use rextendr.", diff --git a/tests/testthat/test-use_rtools.R b/tests/testthat/test-use_rtools.R index fd09f301..bf736cbe 100644 --- a/tests/testthat/test-use_rtools.R +++ b/tests/testthat/test-use_rtools.R @@ -65,7 +65,7 @@ test_that("throw_if_no_rtools does not throw when Rtools is found", { test_that("throw_if_not_ucrt throws when R is not UCRT", { abort_mock <- mockery::mock(stop("Aborted in test")) - mockery::stub(throw_if_not_ucrt, "get_r_version", list(ucrt = "non-ucrt")) + mockery::stub(throw_if_not_ucrt, "get_r_version", list(crt = "non-ucrt")) mockery::stub(throw_if_not_ucrt, "cli::cli_abort", abort_mock) expect_error(throw_if_not_ucrt(), "Aborted in test") @@ -78,7 +78,7 @@ test_that("throw_if_not_ucrt throws when R is not UCRT", { test_that("throw_if_not_ucrt does not throw when R is UCRT", { abort_mock <- mockery::mock(stop("Aborted in test")) - mockery::stub(throw_if_not_ucrt, "get_r_version", list(ucrt = "ucrt")) + mockery::stub(throw_if_not_ucrt, "get_r_version", list(crt = "ucrt")) mockery::stub(throw_if_not_ucrt, "cli::cli_abort", abort_mock) expect_silent(throw_if_not_ucrt()) From a651278870884e3ccca813eb718a7cea713dcc62 Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Tue, 28 Oct 2025 20:27:51 +0200 Subject: [PATCH 12/14] Fix use_rtools bug --- R/use_rtools.R | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/R/use_rtools.R b/R/use_rtools.R index 384b5479..d7892fe5 100644 --- a/R/use_rtools.R +++ b/R/use_rtools.R @@ -96,12 +96,12 @@ get_rtools_home <- function(rtools_version, is_arm) { get_rtools_bin_path <- function(rtools_home, is_arm) { # c.f. https://github.com/wch/r-source/blob/f09d3d7fa4af446ad59a375d914a0daf3ffc4372/src/library/profile/Rprofile.windows#L70-L71 # nolint: line_length_linter subdir <- if (is_arm) { - c("aarch64-w64-mingw32.static.posix", "usr", "bin") + c("aarch64-w64-mingw32.static.posix", "usr") } else { - c("x86_64-w64-mingw32.static.posix", "usr", "bin") + c("x86_64-w64-mingw32.static.posix", "usr") } - normalizePath(file.path(rtools_home, subdir), mustWork = TRUE) + normalizePath(file.path(rtools_home, subdir, "bin"), mustWork = TRUE) } use_rtools <- function(.local_envir = parent.frame()) { From 69af46338f0f3d1977538937bc1b2cbc6fd9a045 Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Tue, 28 Oct 2025 20:46:08 +0200 Subject: [PATCH 13/14] Update test --- tests/testthat/test-use_rtools.R | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/testthat/test-use_rtools.R b/tests/testthat/test-use_rtools.R index bf736cbe..81772e44 100644 --- a/tests/testthat/test-use_rtools.R +++ b/tests/testthat/test-use_rtools.R @@ -192,8 +192,8 @@ patrick::with_parameters_test_that("get_rtools_bin_path:", result <- get_rtools_bin_path(rtools_home, is_arm) expect_equal(result, expected_path) - expected_arg <- c(subdir, "usr", "bin") - mockery::expect_args(file_path_mock, 1, rtools_home, expected_arg) + expected_arg <- c(subdir, "usr") + mockery::expect_args(file_path_mock, 1, rtools_home, expected_arg, "bin") mockery::expect_args(normalize_path_mock, 1, file_path_result, mustWork = TRUE) }, is_arm = c(TRUE, FALSE), From a1c372b587d02ed9e59c035df9fe724603762175 Mon Sep 17 00:00:00 2001 From: Ilia Kosenkov Date: Tue, 28 Oct 2025 20:52:42 +0200 Subject: [PATCH 14/14] Update NEWS --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index 698d04f0..384d24e9 100644 --- a/NEWS.md +++ b/NEWS.md @@ -8,6 +8,10 @@ * Unknown macro options in dev and release now throw errors instead of warnings * `vendor_pkgs()` now has a `clean` argument to remove the `src/rust/vendor` directory after creating the `vendor.tar.xz` file. (#479) * `Makevars`(.win) now uses the `vendor/`, if it exists, before unzipping the tarball. (#479) +* Enhanced runtime compilation with `rust_source()` family of functions (#481) + * Dropped support for 32-bit Windows target + * Added support for ARM64 Windows target + # rextendr 0.4.2