From 26ad94c08731ece5748a227c19814b3402b33820 Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 24 Feb 2026 16:12:59 -0700 Subject: [PATCH 01/15] Drop pre-2.35 CmdStan support Raise cmdstanr support floors and remove legacy Windows toolchain compatibility paths tied to older CmdStan versions. Core behavior updates: - Require CmdStan >= 2.35.0 across installation and path initialization flows. - Enforce version floor for install_cmdstan() inputs (version, release_url/release_file when parseable) and after archive unpack. - Reject unsupported existing installations in set_cmdstan_path()/cmdstanr_initialize(). - Keep CMDSTANR_USE_MSYS_TOOLCHAIN as deprecated/ignored with a once-per-session warning and migration guidance. Windows/toolchain cleanup: - Remove legacy MSYS/pacman/mingw32-make toolchain-install code paths. - Remove Rtools35/R3.x-specific branching and simplify toolchain path selection for current Rtools flows. R floor + compatibility cleanup: - Bump package Depends: R to >= 4.0.0. - Remove obsolete tempdir branches for R < 3.5 in code/tests. Docs and tests: - Update NEWS and install documentation, including note about using older cmdstanr releases for older CmdStan/MSYS workflows. - Add tests for archive version extraction, unsupported version rejection (including release_file and RC boundary), unsupported CMDSTAN env initialization, and ignored MSYS env warning behavior. --- DESCRIPTION | 2 +- NEWS.md | 5 + R/args.R | 8 +- R/install.R | 257 ++++++------------------- R/path.R | 28 ++- R/utils.R | 35 ++-- R/zzz.R | 6 +- man/install_cmdstan.Rd | 21 +- tests/testthat/test-install.R | 151 +++++++++------ tests/testthat/test-model-output_dir.R | 6 +- tests/testthat/test-path.R | 41 ++++ 11 files changed, 252 insertions(+), 308 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 3280baa04..263357192 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -36,7 +36,7 @@ RoxygenNote: 7.3.3 Roxygen: list(markdown = TRUE, r6 = FALSE) SystemRequirements: CmdStan (https://mc-stan.org/users/interfaces/cmdstan) Depends: - R (>= 3.5.0) + R (>= 4.0.0) Imports: checkmate, data.table, diff --git a/NEWS.md b/NEWS.md index e1b263ab7..b3eea8ec0 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # cmdstanr (development version) +* CmdStan versions older than 2.35.0 are no longer supported. +* Package minimum R version increased to 4.0.0. +* Removed legacy Windows toolchain paths for older CmdStan releases. +* `CMDSTANR_USE_MSYS_TOOLCHAIN` is now deprecated and ignored (with a warning). + * Removed deprecated items (replacements in parentheses): - `read_sample_csv()` (`read_cmdstan_csv()`) - `write_stan_tempfile()` (`write_stan_file()`) diff --git a/R/args.R b/R/args.R index 6373eb07a..e84d141f8 100644 --- a/R/args.R +++ b/R/args.R @@ -68,14 +68,8 @@ CmdStanArgs <- R6::R6Class( self$output_dir <- ifelse(is.null(output_dir), file.path(wsl_dir_prefix(), wsl_tempdir()), wsl_safe_path(output_dir)) - } else if (getRversion() < "3.5.0") { - self$output_dir <- output_dir %||% tempdir() } else { - if (getRversion() < "3.5.0") { - self$output_dir <- output_dir %||% tempdir() - } else { - self$output_dir <- output_dir %||% tempdir(check = TRUE) - } + self$output_dir <- output_dir %||% tempdir(check = TRUE) } self$output_dir <- repair_path(self$output_dir) self$output_basename <- output_basename diff --git a/R/install.R b/R/install.R index a75ff5cbe..4adf6076a 100644 --- a/R/install.R +++ b/R/install.R @@ -20,16 +20,11 @@ #' #' The `check_cmdstan_toolchain()` function attempts to check for the required #' C++ toolchain. It is called internally by `install_cmdstan()` but can also -#' be called directly by the user. On Windows only, calling the function with -#' the `fix = TRUE` argument will attempt to install the necessary toolchain -#' components if they are not found. For Windows users with RTools and CmdStan -#' versions >= 2.35 no additional toolchain configuration is required. +#' be called directly by the user. #' -#' NOTE: When installing CmdStan on Windows with RTools and CmdStan versions -#' prior to 2.35.0, the above additional toolchain configuration -#' is still required. To enable this configuration, set the environment variable -#' `CMDSTANR_USE_MSYS_TOOLCHAIN` to 'true' and call -#' `check_cmdstan_toolchain(fix = TRUE)`. +#' CmdStan versions older than 2.35.0 are no longer supported. If you need to +#' work with an older CmdStan version we recommend installing an older +#' CmdStanR release from GitHub. #' #' @export #' @param dir (string) The path to the directory in which to install CmdStan. @@ -57,11 +52,11 @@ #' @param release_url (string) The URL for the specific CmdStan release or #' release candidate to install. See . #' The URL should point to the tarball (`.tar.gz.` file) itself, e.g., -#' `release_url="https://github.com/stan-dev/cmdstan/releases/download/v2.25.0/cmdstan-2.25.0.tar.gz"`. +#' `release_url="https://github.com/stan-dev/cmdstan/releases/download/v2.35.0/cmdstan-2.35.0.tar.gz"`. #' If both `version` and `release_url` are specified then `version` will be used. #' @param release_file (string) A file path to a CmdStan release tar.gz file #' downloaded from the releases page: . -#' For example: `release_file=""./cmdstan-2.33.1.tar.gz"`. If `release_file` is +#' For example: `release_file=""./cmdstan-2.35.0.tar.gz"`. If `release_file` is #' specified then both `release_url` and `version` will be ignored. #' @param cpp_options (list) Any makefile flags/variables to be written to #' the `make/local` file. For example, `list("CXX" = "clang++")` will force @@ -98,6 +93,7 @@ install_cmdstan <- function(dir = NULL, cpp_options = list(), check_toolchain = TRUE, wsl = FALSE) { + warn_if_ignored_msys_toolchain_env() # Use environment variable to record WSL usage throughout install, # post-installation will simply check for 'wsl-' prefix in cmdstan path if (isTRUE(wsl)) { @@ -111,15 +107,6 @@ install_cmdstan <- function(dir = NULL, } else { .cmdstanr$WSL <- FALSE } - if (os_is_windows() && !os_is_wsl() && isTRUE(version < "2.35.0")) { - # RTools can be used unmodified with CmdStan 2.35+ - # For new installs of older versions, users need to install mingw32-make and MSYS gcc - if (Sys.getenv("CMDSTANR_USE_MSYS_TOOLCHAIN") == "") { - stop("CmdStan versions prior to 2.35.0 require additional toolchain configuration on Windows.\n", - "Please set the environment variable CMDSTANR_USE_MSYS_TOOLCHAIN to 'true' and \n", - "call `check_cmdstan_toolchain(fix = TRUE)` before installing CmdStan.", call. = FALSE) - } - } if (check_toolchain) { check_cmdstan_toolchain(fix = FALSE, quiet = quiet) } @@ -149,6 +136,7 @@ install_cmdstan <- function(dir = NULL, release_url <- release_file } if (!is.null(version) && is.null(release_file)) { + assert_supported_requested_cmdstan_version(version, source = "version") if (!is.null(release_url)) { warning("version and release_url shouldn't both be specified!", "\nrelease_url will be ignored.", call. = FALSE) @@ -158,6 +146,10 @@ install_cmdstan <- function(dir = NULL, version, "/cmdstan-", version, cmdstan_arch_suffix(version), ".tar.gz") } if (!is.null(release_url)) { + release_ver <- extract_cmdstan_version_from_archive_name(release_url) + if (!is.null(release_ver)) { + assert_supported_requested_cmdstan_version(release_ver, source = "release_url/release_file") + } if (!endsWith(release_url, ".tar.gz")) { stop(release_url, " is not a .tar.gz archive!", "cmdstanr supports installing from .tar.gz archives only.", call. = FALSE) @@ -173,6 +165,7 @@ install_cmdstan <- function(dir = NULL, dest_file <- file.path(dir, tar_gz_file) } else { ver <- latest_released_version(quiet = quiet) + assert_supported_requested_cmdstan_version(ver, source = "latest release") message("* Latest CmdStan release is v", ver) cmdstan_ver <- paste0("cmdstan-", ver) tar_gz_file <- paste0(cmdstan_ver, cmdstan_arch_suffix(ver), ".tar.gz") @@ -227,6 +220,10 @@ install_cmdstan <- function(dir = NULL, } file.remove(dest_file) } + extracted_version <- suppressWarnings(read_cmdstan_version(dir_cmdstan)) + if (!is.null(extracted_version)) { + assert_supported_requested_cmdstan_version(extracted_version, source = "archive") + } cmdstan_make_local(dir = dir_cmdstan, cpp_options = cpp_options, append = TRUE) # Setting up native M1 compilation of CmdStan and its downstream libraries @@ -349,18 +346,16 @@ cmdstan_make_local <- function(dir = cmdstan_path(), #' @rdname install_cmdstan #' @export #' @param fix For `check_cmdstan_toolchain()`, should CmdStanR attempt to fix -#' any detected toolchain problems? Currently this option is only available on -#' Windows. The default is `FALSE`, in which case problems are only reported -#' along with suggested fixes. +#' any detected toolchain problems? The default is `FALSE`. +#' This argument is currently ignored and retained for compatibility. #' check_cmdstan_toolchain <- function(fix = FALSE, quiet = FALSE) { + warn_if_ignored_msys_toolchain_env() if (os_is_windows()) { if (os_is_wsl()) { check_wsl_toolchain() - } else if (R.version$major >= "4") { - check_rtools4x_windows_toolchain(fix = fix, quiet = quiet) } else { - check_rtools35_windows_toolchain(fix = fix, quiet = quiet) + check_rtools4x_windows_toolchain(fix = fix, quiet = quiet) } } else { check_unix_make() @@ -575,36 +570,6 @@ build_status_ok <- function(process_log, quiet = FALSE) { TRUE } -install_toolchain <- function(quiet = FALSE) { - rtools_usr_bin <- file.path(rtools4x_home_path(), "usr", "bin") - rtools_version <- paste0("Rtools", rtools4x_version()) - if (is_ucrt_toolchain()) { - install_pkgs <- c("mingw-w64-ucrt-x86_64-make", "mingw-w64-ucrt-x86_64-gcc") - if (!quiet) message(paste0("Installing mingw32-make and g++ with ", rtools_version)) - } else { - install_pkgs <- "mingw-w64-x86_64-make" - if (!quiet) message(paste0("Installing mingw32-make with ", rtools_version)) - } - if (!checkmate::test_directory(rtools_usr_bin, access = "w")) { - warning("No write permissions in the RTools folder. This might prevent installing the toolchain.", - " Consider changing permissions or reinstalling RTools in a different folder.", call. = FALSE) - } - withr::with_path( - c( - toolchain_PATH_env_var() - ), - processx::run( - "pacman", - args = c("-Sy", install_pkgs, "--noconfirm"), - wd = rtools_usr_bin, - error_on_status = TRUE, - echo_cmd = is_verbose_mode(), - echo = is_verbose_mode() - ) - ) - invisible(NULL) -} - check_wsl_toolchain <- function() { if (!wsl_installed()) { stop("\n", "A WSL distribution is not installed or is not accessible.", @@ -638,9 +603,8 @@ check_wsl_toolchain <- function() { } check_rtools4x_windows_toolchain <- function(fix = FALSE, quiet = FALSE) { - rtools_path <- rtools_home_path() + rtools_path <- rtools4x_home_path() rtools_version <- paste0("Rtools", rtools4x_version()) - toolchain_path <- rtools4x_toolchain_path() # If RTOOLS4X_HOME is not set (the env. variable gets set on install) # we assume that RTools 40 is not installed. if (!nzchar(rtools_path)) { @@ -660,92 +624,7 @@ check_rtools4x_windows_toolchain <- function(fix = FALSE, quiet = FALSE) { call. = FALSE ) } - # No additional utilities/toolchains are needed with RTools4 - if (Sys.getenv("CMDSTANR_USE_MSYS_TOOLCHAIN") == "") { - return(invisible(NULL)) - } - if (!is_toolchain_installed(app = "g++", path = toolchain_path) || - !is_toolchain_installed(app = "mingw32-make", path = toolchain_path)) { - if (!fix) { - stop( - "\n", rtools_version, " installation found but the toolchain was not installed.", - "\nRun cmdstanr::check_cmdstan_toolchain(fix = TRUE) to fix the issue.", - call. = FALSE - ) - } else { - install_toolchain(quiet = quiet) - if (!is_toolchain_installed(app = "g++", path = toolchain_path) || - !is_toolchain_installed(app = "mingw32-make", path = toolchain_path)) { - stop( - "\nInstallation of the toolchain failed. Try reinstalling RTools and trying again.", - "\nIf the issue persists, open a bug report at https://github.com/stan-dev/cmdstanr.", - call. = FALSE - ) - } - return(invisible(NULL)) - } - } -} - -check_rtools35_windows_toolchain <- function(fix = FALSE, - quiet = FALSE, - paths = NULL) { - if (is.null(paths)) { - paths <- c(file.path("C:/", "Rtools"), file.path("C:/", "Rtools35")) - } - mingw32_make_path <- dirname(Sys.which("mingw32-make")) - gpp_path <- dirname(Sys.which("g++")) - # If mingw32-make and g++ are not found, we check typical RTools 3.5 folders. - # If found, we fix PATH, otherwise we recommend the user to install RTools 3.5. - if (!nzchar(mingw32_make_path) || !nzchar(gpp_path)) { - rtools_path <- Sys.getenv("RTOOLS35_HOME") - if (!nzchar(rtools_path)) { - found_rtools <- FALSE - for (p in paths) { - if (dir.exists(p)) { - if (found_rtools) { - stop( - "\nMultiple RTools 3.5 installations found. Please select the installation to use by running", - "\n\nwrite(\'RTOOLS35_HOME=rtools35/install/path/\', file = \"~/.Renviron\", append = TRUE)", - "\n\nThen restart R and run 'cmdstanr::check_cmdstan_toolchain(fix = TRUE)'.", - call. = FALSE - ) - } else { - rtools_path <- p - found_rtools <- TRUE - } - } - } - } - if (nzchar(rtools_path)) { - if (!fix) { - stop( - "\nRTools installation found but PATH was not properly set.", - "\nRun check_cmdstan_toolchain(fix = TRUE) to fix the issue.", - call. = FALSE - ) - } - if (!quiet) { - message("Writing RTools path to ~/.Renviron ...") - } - if (!nzchar(Sys.getenv("RTOOLS35_HOME"))) { - write(paste0("RTOOLS35_HOME=", rtools_path), file = "~/.Renviron", append = TRUE) - Sys.setenv(RTOOLS35_HOME = rtools_path) - } - write('PATH="${RTOOLS35_HOME}\\bin;${RTOOLS35_HOME}\\mingw_64\\bin;${PATH}"', file = "~/.Renviron", append = TRUE) - Sys.setenv(PATH = paste0(Sys.getenv("RTOOLS35_HOME"), "\\mingw_64\\bin;", Sys.getenv("PATH"))) - check_rtools35_windows_toolchain(fix = FALSE, quiet = quiet) - return(invisible(NULL)) - } else { - stop( - "\nA toolchain was not found. Please install RTools 3.5 and run", - "\n\nwrite(\'RTOOLS35_HOME=rtools35/install/path/\', file = \"~/.Renviron\", append = TRUE)", - "\nreplacing 'rtools35/install/path/' with the actual install path of RTools 3.5.", - "\n\nThen restart R and run 'cmdstanr::check_cmdstan_toolchain(fix = TRUE)'.", - call. = FALSE - ) - } - } + invisible(NULL) } check_unix_make <- function() { @@ -827,61 +706,25 @@ cmdstan_arch_suffix <- function(version = NULL) { paste0("-linux-", selected_arch) } -is_toolchain_installed <- function(app, path) { - res <- tryCatch({ - withr::with_path( - c( - toolchain_PATH_env_var() - ), - processx::run( - app, - args = c("--version"), - wd = path, - error_on_status = FALSE, - echo_cmd = is_verbose_mode(), - echo = is_verbose_mode() - ) - ) - app_path <- withr::with_path( - c( - toolchain_PATH_env_var() - ), - repair_path(dirname(Sys.which(app))) - ) - if (normalizePath(app_path) != normalizePath(rtools4x_toolchain_path())) { - return(FALSE) - } - return(TRUE) - }, - error = function(cond) { - return(FALSE) - } - ) - res -} - toolchain_PATH_env_var <- function() { - path <- NULL - if (R.version$major == "4") { - rtools_home <- rtools4x_home_path() - path <- paste0( - repair_path(file.path(rtools_home, "usr", "bin")), ";", - rtools4x_toolchain_path() - ) + if (!os_is_windows()) { + return(NULL) } - path + rtools_home <- rtools4x_home_path() + if (!nzchar(rtools_home)) { + return(NULL) + } + paste0( + repair_path(file.path(rtools_home, "usr", "bin")), ";", + rtools4x_toolchain_path() + ) } rtools4x_toolchain_path <- function() { if (arch_is_aarch64()) { toolchain <- "aarch64-w64-mingw32.static.posix" } else { - if (Sys.getenv("CMDSTANR_USE_MSYS_TOOLCHAIN") != "" || - isTRUE(cmdstan_version(error_on_NA=FALSE) < "2.35.0")) { - toolchain <- ifelse(is_ucrt_toolchain(), "ucrt64", "mingw64") - } else { - toolchain <- "x86_64-w64-mingw32.static.posix" - } + toolchain <- "x86_64-w64-mingw32.static.posix" } repair_path(file.path(rtools4x_home_path(), toolchain, "bin")) } @@ -923,13 +766,29 @@ rtools4x_home_path <- function() { path } -rtools_home_path <- function() { - path <- NULL - if (R.version$major == "3") { - path <- Sys.getenv("RTOOLS_HOME") - } - if (R.version$major == "4") { - path <- rtools4x_home_path() +assert_supported_requested_cmdstan_version <- function(version, source = "version") { + if (is_supported_cmdstan_version(version)) { + return(invisible(NULL)) } - path + stop( + "Requested CmdStan ", source, " (", version, ") is unsupported. ", + "cmdstanr now requires CmdStan v", cmdstan_min_version(), " or newer. ", + "If you need an older CmdStan release, install an older cmdstanr version from GitHub.", + call. = FALSE + ) +} + +extract_cmdstan_version_from_archive_name <- function(path_or_url) { + archive <- basename(path_or_url) + archive <- sub("\\?.*$", "", archive) + matches <- regmatches( + archive, + regexec("^cmdstan-([0-9]+\\.[0-9]+\\.[0-9]+(?:-rc[0-9]+)?)(?:-linux-[a-z0-9_]+)?\\.tar\\.gz$", + archive, + perl = TRUE) + )[[1]] + if (length(matches) >= 2) { + return(matches[2]) + } + NULL } diff --git a/R/path.R b/R/path.R index d754be92e..26a0aedf6 100644 --- a/R/path.R +++ b/R/path.R @@ -43,8 +43,19 @@ set_cmdstan_path <- function(path = NULL) { } if (dir.exists(path)) { path <- absolute_path(path) + version <- read_cmdstan_version(path) + if (!is.null(version) && !is_supported_cmdstan_version(version)) { + warning( + "CmdStan path not set. CmdStan v", version, " is no longer supported. ", + "cmdstanr now requires CmdStan v", cmdstan_min_version(), " or newer.", + call. = FALSE + ) + .cmdstanr$PATH <- NULL + .cmdstanr$VERSION <- NULL + return(invisible(path)) + } .cmdstanr$PATH <- path - .cmdstanr$VERSION <- read_cmdstan_version(path) + .cmdstanr$VERSION <- version .cmdstanr$WSL <- grepl("//wsl$", path, fixed = TRUE) message("CmdStan path set to: ", path) } else { @@ -100,6 +111,21 @@ stop_no_path <- function() { call. = FALSE) } +cmdstan_min_version <- function() { + "2.35.0" +} + +is_supported_cmdstan_version <- function(version) { + if (is.null(version)) { + return(FALSE) + } + cmp <- suppressWarnings(utils::compareVersion(as.character(version), cmdstan_min_version())) + if (is.na(cmp)) { + return(FALSE) + } + cmp >= 0 +} + #' cmdstan_default_install_path #' #' Path to where [install_cmdstan()] with default settings installs CmdStan. diff --git a/R/utils.R b/R/utils.R index 94a531373..8cf555c6e 100644 --- a/R/utils.R +++ b/R/utils.R @@ -58,18 +58,6 @@ os_is_linux <- function() { isTRUE(Sys.info()[["sysname"]] == "Linux") } -is_rtools43_toolchain <- function() { - os_is_windows() && R.version$major == "4" && R.version$minor >= "3.0" -} - -is_rtools42_toolchain <- function() { - os_is_windows() && R.version$major == "4" && R.version$minor >= "2.0" && R.version$minor < "3.0" -} - -is_rtools40_toolchain <- function() { - os_is_windows() && R.version$major == "4" && R.version$minor < "2.0" -} - is_ucrt_toolchain <- function() { os_is_windows() && R.version$major == "4" && R.version$minor >= "2.0" } @@ -94,16 +82,33 @@ arch_is_aarch64 <- function() { # Returns the type of make command to use to compile depending on the OS # First checks if $MAKE is set, otherwise falls back to system-specific default make_cmd <- function() { + warn_if_ignored_msys_toolchain_env() if (Sys.getenv("MAKE") != "") { Sys.getenv("MAKE") - } else if (os_is_windows() && !os_is_wsl() && - (Sys.getenv("CMDSTANR_USE_MSYS_TOOLCHAIN") != "" || isTRUE(cmdstan_version(error_on_NA=FALSE) < "2.35.0"))) { - "mingw32-make.exe" } else { "make" } } +warn_if_ignored_msys_toolchain_env <- function() { + if (Sys.getenv("CMDSTANR_USE_MSYS_TOOLCHAIN") == "") { + return(invisible(NULL)) + } + # Keep this warning to once per R session because this helper is called + # from high-frequency internal paths (e.g., make/toolchain resolution). + if (isTRUE(.cmdstanr$WARNED_IGNORED_MSYS_TOOLCHAIN)) { + return(invisible(NULL)) + } + warning( + "Environment variable 'CMDSTANR_USE_MSYS_TOOLCHAIN' is deprecated and ignored. ", + "cmdstanr now requires CmdStan v", cmdstan_min_version(), " or newer.\n", + "If you need legacy MSYS toolchain support, use an older cmdstanr release.", + call. = FALSE + ) + .cmdstanr$WARNED_IGNORED_MSYS_TOOLCHAIN <- TRUE + invisible(NULL) +} + # Returns the stanc exe path depending on the OS stanc_cmd <- function() { if (os_is_windows() && !os_is_wsl()) { diff --git a/R/zzz.R b/R/zzz.R index 945ca50b3..e6315fc60 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -71,11 +71,7 @@ cmdstanr_initialize <- function() { } } - if (getRversion() < "3.5.0") { - .cmdstanr$TEMP_DIR <- tempdir() - } else { - .cmdstanr$TEMP_DIR <- tempdir(check = TRUE) - } + .cmdstanr$TEMP_DIR <- tempdir(check = TRUE) invisible(TRUE) } diff --git a/man/install_cmdstan.Rd b/man/install_cmdstan.Rd index 564ed9e14..b09a02bb0 100644 --- a/man/install_cmdstan.Rd +++ b/man/install_cmdstan.Rd @@ -88,9 +88,8 @@ makefile flags be appended to the end of the existing \code{make/local} file? The default is \code{TRUE}. If \code{FALSE} the file is overwritten.} \item{fix}{For \code{check_cmdstan_toolchain()}, should CmdStanR attempt to fix -any detected toolchain problems? Currently this option is only available on -Windows. The default is \code{FALSE}, in which case problems are only reported -along with suggested fixes.} +any detected toolchain problems? The default is \code{FALSE}. +This argument is currently ignored and retained for compatibility.} } \value{ For \code{cmdstan_make_local()}, if \code{cpp_options=NULL} then the existing @@ -118,16 +117,12 @@ should typically be followed by calling \code{rebuild_cmdstan()}. The \code{check_cmdstan_toolchain()} function attempts to check for the required C++ toolchain. It is called internally by \code{install_cmdstan()} but can also -be called directly by the user. On Windows only, calling the function with -the \code{fix = TRUE} argument will attempt to install the necessary toolchain -components if they are not found. For Windows users with RTools and CmdStan -versions >= 2.35 no additional toolchain configuration is required. - -NOTE: When installing CmdStan on Windows with RTools and CmdStan versions -prior to 2.35.0, the above additional toolchain configuration -is still required. To enable this configuration, set the environment variable -\code{CMDSTANR_USE_MSYS_TOOLCHAIN} to 'true' and call -\code{check_cmdstan_toolchain(fix = TRUE)}. +be called directly by the user. + +CmdStan versions older than 2.35.0 are no longer supported by cmdstanr. +If you need to work with an older CmdStan version, install an older cmdstanr +release from GitHub, e.g. +\code{remotes::install_github("stan-dev/cmdstanr@v0.8.1")}. } \examples{ \dontrun{ diff --git a/tests/testthat/test-install.R b/tests/testthat/test-install.R index ad7ad069e..28334ce68 100644 --- a/tests/testthat/test-install.R +++ b/tests/testthat/test-install.R @@ -9,11 +9,7 @@ if (!nzchar(cmdstan_test_tarball_url)) { } test_that("install_cmdstan() successfully installs cmdstan", { - if (getRversion() < '3.5.0') { - dir <- tempdir() - } else { - dir <- tempdir(check = TRUE) - } + dir <- tempdir(check = TRUE) expect_message( expect_output( install_cmdstan(dir = dir, cores = CORES, quiet = FALSE, overwrite = TRUE, @@ -44,11 +40,7 @@ test_that("install_cmdstan() errors if installation already exists", { test_that("install_cmdstan() errors if it times out", { skip_if(!is.null(cmdstan_test_tarball_url)) - if (getRversion() < '3.5.0') { - dir <- tempdir() - } else { - dir <- tempdir(check = TRUE) - } + dir <- tempdir(check = TRUE) ver <- latest_released_version() dir_exists <- dir.exists(file.path(dir, paste0("cmdstan-",ver))) # with quiet=TRUE @@ -96,11 +88,7 @@ test_that("install_cmdstan() works with version and release_url", { # this test is irrelevant if tests are using a release candidate tarball URL so skip skip_if(!is.null(cmdstan_test_tarball_url)) - if (getRversion() < '3.5.0') { - dir <- tempdir() - } else { - dir <- tempdir(check = TRUE) - } + dir <- tempdir(check = TRUE) expect_message( expect_output( @@ -158,38 +146,6 @@ test_that("toolchain checks on Unix work", { Sys.setenv("PATH" = path_backup) }) -test_that("toolchain checks on Windows with RTools 3.5 work", { - skip_if_not(os_is_windows()) - skip_if(os_is_wsl()) - skip_if(R.Version()$major > "3") - - path_backup <- Sys.getenv("PATH") - Sys.setenv("PATH" = "") - tmpdir <- tempdir() - tmp_dir1 <- file.path(tmpdir, "dir1") - tmp_dir2 <- file.path(tmpdir, "dir2") - if (dir.exists(tmp_dir1)) unlink(tmp_dir1) - if (dir.exists(tmp_dir2)) unlink(tmp_dir2) - expect_error( - check_rtools35_windows_toolchain(paths= c(tmp_dir1, tmp_dir2)), - "\nA toolchain was not found. Please install RTools 3.5 and run", - fixed = TRUE - ) - if (!dir.exists(tmp_dir1)) dir.create(tmp_dir1) - expect_error( - check_rtools35_windows_toolchain(paths= c(tmp_dir1, tmp_dir2)), - "\nRTools installation found but PATH was not properly set.", - fixed = TRUE - ) - if (!dir.exists(tmp_dir2)) dir.create(tmp_dir2) - expect_error( - check_rtools35_windows_toolchain(paths= c(tmp_dir1, tmp_dir2)), - "\nMultiple RTools 3.5 installations found. Please select the installation to use", - fixed = TRUE - ) - Sys.setenv("PATH" = path_backup) -}) - test_that("clean and rebuild works", { expect_output( rebuild_cmdstan(cores = CORES), @@ -205,12 +161,40 @@ test_that("github_download_url constructs correct url", { ) }) +test_that("extract_cmdstan_version_from_archive_name parses realistic inputs", { + expect_equal( + extract_cmdstan_version_from_archive_name( + "https://github.com/stan-dev/cmdstan/releases/download/v2.36.0/cmdstan-2.36.0.tar.gz" + ), + "2.36.0" + ) + expect_equal( + extract_cmdstan_version_from_archive_name( + "https://github.com/stan-dev/cmdstan/releases/download/v2.36.0/cmdstan-2.36.0-linux-arm64.tar.gz" + ), + "2.36.0" + ) + expect_equal( + extract_cmdstan_version_from_archive_name( + "https://github.com/stan-dev/cmdstan/releases/download/v2.35.0-rc1/cmdstan-2.35.0-rc1.tar.gz?download=1" + ), + "2.35.0-rc1" + ) + expect_equal( + extract_cmdstan_version_from_archive_name( + file.path(tempdir(check = TRUE), "cmdstan-2.35.1-linux-s390x.tar.gz") + ), + "2.35.1" + ) + expect_null( + extract_cmdstan_version_from_archive_name( + "https://github.com/stan-dev/cmdstan/releases/tag/v2.36.0" + ) + ) +}) + test_that("Downloads respect quiet argument", { - if (getRversion() < '3.5.0') { - dir <- tempdir() - } else { - dir <- tempdir(check = TRUE) - } + dir <- tempdir(check = TRUE) version <- latest_released_version() ver_msg <- "trying URL 'https://api.github.com/repos/stan-dev/cmdstan/releases/latest'" @@ -239,11 +223,7 @@ test_that("Download failures return error message", { # GHA fails on Windows old-rel here, but cannot replicate locally skip_if(os_is_windows() && getRversion() < '4.2') - if (getRversion() < '3.5.0') { - dir <- tempdir() - } else { - dir <- tempdir(check = TRUE) - } + dir <- tempdir(check = TRUE) expect_error({ # Use an invalid proxy address to force a download failure @@ -255,11 +235,7 @@ test_that("Download failures return error message", { }) test_that("Install from release file works", { - if (getRversion() < '3.5.0') { - dir <- tempdir() - } else { - dir <- tempdir(check = TRUE) - } + dir <- tempdir(check = TRUE) destfile <- file.path(dir, "cmdstan-2.36.0.tar.gz") @@ -279,3 +255,54 @@ test_that("Install from release file works", { fixed = TRUE ) }) + +test_that("install_cmdstan() errors for unsupported CmdStan versions", { + expect_error( + install_cmdstan(version = "2.34.0", check_toolchain = FALSE, wsl = os_is_wsl()), + "Requested CmdStan version (2.34.0) is unsupported.", + fixed = TRUE + ) + expect_error( + install_cmdstan( + release_url = "https://github.com/stan-dev/cmdstan/releases/download/v2.34.0/cmdstan-2.34.0.tar.gz", + check_toolchain = FALSE, + wsl = os_is_wsl() + ), + "Requested CmdStan release_url/release_file (2.34.0) is unsupported.", + fixed = TRUE + ) + expect_error( + install_cmdstan( + release_file = file.path(tempdir(check = TRUE), "cmdstan-2.34.0.tar.gz"), + check_toolchain = FALSE, + wsl = os_is_wsl() + ), + "Requested CmdStan release_url/release_file (2.34.0) is unsupported.", + fixed = TRUE + ) +}) + +test_that("unsupported release-candidate versions are rejected by the floor check", { + expect_false(is_supported_cmdstan_version("2.34.0-rc1")) + expect_true(is_supported_cmdstan_version("2.35.0-rc1")) + expect_error( + install_cmdstan(version = "2.34.0-rc1", check_toolchain = FALSE, wsl = os_is_wsl()), + "Requested CmdStan version (2.34.0-rc1) is unsupported.", + fixed = TRUE + ) +}) + +test_that("deprecated CMDSTANR_USE_MSYS_TOOLCHAIN is ignored with warning", { + old_flag <- .cmdstanr$WARNED_IGNORED_MSYS_TOOLCHAIN + on.exit(.cmdstanr$WARNED_IGNORED_MSYS_TOOLCHAIN <- old_flag) + + .cmdstanr$WARNED_IGNORED_MSYS_TOOLCHAIN <- FALSE + withr::with_envvar(c(CMDSTANR_USE_MSYS_TOOLCHAIN = "true"), { + expect_warning( + make_cmd(), + "CMDSTANR_USE_MSYS_TOOLCHAIN", + fixed = TRUE + ) + expect_silent(make_cmd()) + }) +}) diff --git a/tests/testthat/test-model-output_dir.R b/tests/testthat/test-model-output_dir.R index cfa6a5ec1..f52848276 100644 --- a/tests/testthat/test-model-output_dir.R +++ b/tests/testthat/test-model-output_dir.R @@ -1,11 +1,7 @@ context("model-output_dir-output-basename") set_cmdstan_path() -if (getRversion() < '3.5.0') { - sandbox <- file.path(tempdir(), "sandbox") -} else { - sandbox <- file.path(tempdir(check = TRUE), "sandbox") -} +sandbox <- file.path(tempdir(check = TRUE), "sandbox") if (!dir.exists(sandbox)) { dir.create(sandbox) on.exit(unlink(sandbox, recursive = TRUE)) diff --git a/tests/testthat/test-path.R b/tests/testthat/test-path.R index 03d9f48d7..150d27e03 100644 --- a/tests/testthat/test-path.R +++ b/tests/testthat/test-path.R @@ -44,6 +44,25 @@ test_that("Setting path from env var is detected", { Sys.unsetenv("CMDSTAN") }) +test_that("Unsupported CmdStan path from env var is rejected", { + unset_cmdstan_path() + parent_dir <- file.path(tempdir(check = TRUE), "cmdstan-env-parent") + old_install <- file.path(parent_dir, "cmdstan-2.34.0") + dir.create(old_install, recursive = TRUE, showWarnings = FALSE) + on.exit(unlink(parent_dir, recursive = TRUE), add = TRUE) + on.exit(Sys.unsetenv("CMDSTAN"), add = TRUE) + writeLines("CMDSTAN_VERSION := 2.34.0", con = file.path(old_install, "makefile")) + + Sys.setenv(CMDSTAN = parent_dir) + expect_warning( + cmdstanr_initialize(), + "cmdstanr now requires CmdStan v2.35.0 or newer", + fixed = TRUE + ) + expect_null(.cmdstanr$PATH) + expect_null(.cmdstanr$VERSION) +}) + test_that("cmdstanr_initialize() also looks for default path", { unset_cmdstan_path() cmdstanr_initialize() @@ -90,6 +109,28 @@ test_that("Warning message is thrown if can't detect version number", { ) }) +test_that("Setting path rejects unsupported CmdStan versions", { + old_path <- .cmdstanr$PATH + old_version <- .cmdstanr$VERSION + on.exit({ + .cmdstanr$PATH <- old_path + .cmdstanr$VERSION <- old_version + }) + + path <- file.path(tempdir(check = TRUE), "cmdstan-2.34.0") + dir.create(path, recursive = TRUE, showWarnings = FALSE) + on.exit(unlink(path, recursive = TRUE), add = TRUE) + writeLines("CMDSTAN_VERSION := 2.34.0", con = file.path(path, "makefile")) + + expect_warning( + set_cmdstan_path(path), + "cmdstanr now requires CmdStan v2.35.0 or newer", + fixed = TRUE + ) + expect_null(.cmdstanr$PATH) + expect_null(.cmdstanr$VERSION) +}) + test_that("cmdstan_ext() works", { if (os_is_windows() && !os_is_wsl()) { expect_identical(cmdstan_ext(), ".exe") From 9ef2931454717628ebd8e5d8c3f37f696dc8fc1d Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 24 Feb 2026 16:40:57 -0700 Subject: [PATCH 02/15] simplify is_supported_cmdstan_version() --- R/path.R | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/R/path.R b/R/path.R index 26a0aedf6..5c362a83e 100644 --- a/R/path.R +++ b/R/path.R @@ -119,11 +119,11 @@ is_supported_cmdstan_version <- function(version) { if (is.null(version)) { return(FALSE) } - cmp <- suppressWarnings(utils::compareVersion(as.character(version), cmdstan_min_version())) - if (is.na(cmp)) { - return(FALSE) - } - cmp >= 0 + cmp <- tryCatch( + suppressWarnings(utils::compareVersion(as.character(version), cmdstan_min_version())), + error = function(e) NA_integer_ + ) + isTRUE(cmp >= 0) } #' cmdstan_default_install_path From adffc5dcef8fe88d4bc2880e82ca234a84b1854b Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 24 Feb 2026 16:41:57 -0700 Subject: [PATCH 03/15] Update NEWS.md --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index b3eea8ec0..cd82c6b9c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,7 @@ # cmdstanr (development version) * CmdStan versions older than 2.35.0 are no longer supported. -* Package minimum R version increased to 4.0.0. +* Minimum R version increased to 4.0.0. * Removed legacy Windows toolchain paths for older CmdStan releases. * `CMDSTANR_USE_MSYS_TOOLCHAIN` is now deprecated and ignored (with a warning). From 7ec601e3cae7815c102f6d4080aa7ba2aa0d3c37 Mon Sep 17 00:00:00 2001 From: jgabry Date: Tue, 24 Feb 2026 16:44:10 -0700 Subject: [PATCH 04/15] Update install_cmdstan.Rd --- man/install_cmdstan.Rd | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/man/install_cmdstan.Rd b/man/install_cmdstan.Rd index b09a02bb0..54b7d41c5 100644 --- a/man/install_cmdstan.Rd +++ b/man/install_cmdstan.Rd @@ -64,12 +64,12 @@ is \code{NULL}, which downloads the latest stable release from \item{release_url}{(string) The URL for the specific CmdStan release or release candidate to install. See \url{https://github.com/stan-dev/cmdstan/releases}. The URL should point to the tarball (\code{.tar.gz.} file) itself, e.g., -\code{release_url="https://github.com/stan-dev/cmdstan/releases/download/v2.25.0/cmdstan-2.25.0.tar.gz"}. +\code{release_url="https://github.com/stan-dev/cmdstan/releases/download/v2.35.0/cmdstan-2.35.0.tar.gz"}. If both \code{version} and \code{release_url} are specified then \code{version} will be used.} \item{release_file}{(string) A file path to a CmdStan release tar.gz file downloaded from the releases page: \url{https://github.com/stan-dev/cmdstan/releases}. -For example: \verb{release_file=""./cmdstan-2.33.1.tar.gz"}. If \code{release_file} is +For example: \verb{release_file=""./cmdstan-2.35.0.tar.gz"}. If \code{release_file} is specified then both \code{release_url} and \code{version} will be ignored.} \item{cpp_options}{(list) Any makefile flags/variables to be written to @@ -119,10 +119,9 @@ The \code{check_cmdstan_toolchain()} function attempts to check for the required C++ toolchain. It is called internally by \code{install_cmdstan()} but can also be called directly by the user. -CmdStan versions older than 2.35.0 are no longer supported by cmdstanr. -If you need to work with an older CmdStan version, install an older cmdstanr -release from GitHub, e.g. -\code{remotes::install_github("stan-dev/cmdstanr@v0.8.1")}. +CmdStan versions older than 2.35.0 are no longer supported. If you need to +work with an older CmdStan version we recommend installing an older +CmdStanR release from GitHub. } \examples{ \dontrun{ From de4d8cdbaf87309ee77de6a3149ba3888a3fd66e Mon Sep 17 00:00:00 2001 From: jgabry Date: Wed, 25 Feb 2026 10:04:10 -0700 Subject: [PATCH 05/15] Fix unsupported CMDSTAN env-path test on WSL Avoid backend-specific assumptions that caused failures on WSL - The test had expected cmdstanr_initialize() to leave PATH/VERSION NULL after rejecting an unsupported CmdStan version - On WSL CI, initialization can apparently fall back to an existing supported installation, so NULL is not guaranteed. --- tests/testthat/test-path.R | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/testthat/test-path.R b/tests/testthat/test-path.R index 150d27e03..7aa031b1a 100644 --- a/tests/testthat/test-path.R +++ b/tests/testthat/test-path.R @@ -54,13 +54,12 @@ test_that("Unsupported CmdStan path from env var is rejected", { writeLines("CMDSTAN_VERSION := 2.34.0", con = file.path(old_install, "makefile")) Sys.setenv(CMDSTAN = parent_dir) - expect_warning( - cmdstanr_initialize(), - "cmdstanr now requires CmdStan v2.35.0 or newer", - fixed = TRUE - ) - expect_null(.cmdstanr$PATH) - expect_null(.cmdstanr$VERSION) + suppressWarnings(cmdstanr_initialize()) + expect_false(identical(.cmdstanr$PATH, absolute_path(old_install))) + expect_false(identical(.cmdstanr$VERSION, "2.34.0")) + if (!is.null(.cmdstanr$VERSION)) { + expect_true(is_supported_cmdstan_version(.cmdstanr$VERSION)) + } }) test_that("cmdstanr_initialize() also looks for default path", { From 5c3f8824f1ef231f68e859d164d9617647e22f24 Mon Sep 17 00:00:00 2001 From: jgabry Date: Wed, 25 Feb 2026 16:17:48 -0700 Subject: [PATCH 06/15] Improve Windows toolchain fallback and WSL state handling --- .github/workflows/R-CMD-check.yaml | 25 +++++++ R/install.R | 60 +++++++++++++-- R/path.R | 3 + R/zzz.R | 4 + tests/testthat/test-install.R | 115 +++++++++++++++++++++++++++++ tests/testthat/test-path.R | 20 +++++ 6 files changed, 222 insertions(+), 5 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 0cffb6f81..a4a46ecbe 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -37,6 +37,7 @@ jobs: - {os: macOS-latest, r: 'release', opencl: true} - {os: windows-latest, r: 'devel' } - {os: windows-latest, r: 'release'} + - {os: windows-2022, r: '4.1'} - {os: windows-latest, r: 'oldrel'} - {os: ubuntu-latest, r: 'devel', opencl: true} - {os: ubuntu-latest, r: 'release', opencl: true} @@ -74,6 +75,30 @@ jobs: cache: "always" extra-packages: any::rcmdcheck, local::. + - name: Debug Windows toolchain resolution + if: ${{ runner.os == 'Windows' }} + run: | + rtools_home <- cmdstanr:::rtools4x_home_path() + candidates <- cmdstanr:::rtools4x_toolchain_candidates() + selected <- cmdstanr:::rtools4x_toolchain_path() + cat("R version: ", getRversion(), "\n", sep = "") + cat("Rtools home: ", rtools_home, "\n", sep = "") + cat("Toolchain candidates:\n") + if (length(candidates) == 0) { + cat(" - \n") + } else { + cat(paste0(" - ", candidates), sep = "\n") + cat("\n") + } + cat("Selected toolchain: ", selected, "\n", sep = "") + selected_ok <- nzchar(selected) && + dir.exists(selected) && + any(file.exists(file.path(selected, c("g++.exe", "g++")))) + if (!selected_ok) { + stop("Resolved Windows toolchain path is not usable.") + } + shell: Rscript {0} + - name: Install POCL on Ubuntu Runners if: ${{ matrix.config.os == 'ubuntu-latest' }} run: | diff --git a/R/install.R b/R/install.R index 4adf6076a..d42508b41 100644 --- a/R/install.R +++ b/R/install.R @@ -624,6 +624,31 @@ check_rtools4x_windows_toolchain <- function(fix = FALSE, quiet = FALSE) { call. = FALSE ) } + usr_bin <- repair_path(file.path(rtools_path, "usr", "bin")) + make_found <- any(file.exists(file.path(usr_bin, c("make.exe", "mingw32-make.exe")))) + if (!make_found) { + stop( + "\n", rtools_version, " is missing the required 'make' executable in ", usr_bin, ".", + "\nPlease reinstall ", rtools_version, " and run cmdstanr::check_cmdstan_toolchain().", + call. = FALSE + ) + } + candidates <- rtools4x_toolchain_candidates() + has_usable_toolchain <- any(vapply(candidates, is_rtools4x_toolchain_usable, logical(1))) + if (!has_usable_toolchain) { + if (length(candidates) == 0) { + candidates_message <- "\n- " + } else { + candidates_message <- paste0("\n- ", paste(candidates, collapse = "\n- ")) + } + stop( + "\n", rtools_version, " does not contain a supported C++ toolchain.", + "\nChecked the following paths:", + candidates_message, + "\nPlease reinstall ", rtools_version, " and run cmdstanr::check_cmdstan_toolchain().", + call. = FALSE + ) + } invisible(NULL) } @@ -720,13 +745,38 @@ toolchain_PATH_env_var <- function() { ) } -rtools4x_toolchain_path <- function() { - if (arch_is_aarch64()) { - toolchain <- "aarch64-w64-mingw32.static.posix" +rtools4x_toolchain_candidates <- function() { + rtools_home <- rtools4x_home_path() + if (!nzchar(rtools_home)) { + return(character()) + } + toolchains <- if (arch_is_aarch64()) { + "aarch64-w64-mingw32.static.posix" + } else if (is_ucrt_toolchain()) { + c("x86_64-w64-mingw32.static.posix", "ucrt64", "mingw64") } else { - toolchain <- "x86_64-w64-mingw32.static.posix" + c("x86_64-w64-mingw32.static.posix", "mingw64", "ucrt64") + } + repair_path(file.path(rtools_home, toolchains, "bin")) +} + +is_rtools4x_toolchain_usable <- function(path) { + if (!nzchar(path) || !dir.exists(path)) { + return(FALSE) + } + any(file.exists(file.path(path, c("g++.exe", "g++")))) +} + +rtools4x_toolchain_path <- function() { + candidates <- rtools4x_toolchain_candidates() + if (length(candidates) == 0) { + return("") + } + usable <- vapply(candidates, is_rtools4x_toolchain_usable, logical(1)) + if (any(usable)) { + return(candidates[which(usable)[1]]) } - repair_path(file.path(rtools4x_home_path(), toolchain, "bin")) + candidates[1] } rtools4x_version <- function() { diff --git a/R/path.R b/R/path.R index 5c362a83e..fdd9adcf0 100644 --- a/R/path.R +++ b/R/path.R @@ -52,6 +52,7 @@ set_cmdstan_path <- function(path = NULL) { ) .cmdstanr$PATH <- NULL .cmdstanr$VERSION <- NULL + .cmdstanr$WSL <- FALSE return(invisible(path)) } .cmdstanr$PATH <- path @@ -99,6 +100,7 @@ cmdstan_version <- function(error_on_NA = TRUE) { .cmdstanr$PATH <- NULL .cmdstanr$VERSION <- NULL .cmdstanr$TEMP_DIR <- NULL +.cmdstanr$WSL <- FALSE # path to temp directory cmdstan_tempdir <- function() { @@ -250,6 +252,7 @@ is_release_candidate <- function(path) { unset_cmdstan_path <- function() { .cmdstanr$PATH <- NULL .cmdstanr$VERSION <- NULL + .cmdstanr$WSL <- FALSE } # fake a cmdstan version (only used in tests) diff --git a/R/zzz.R b/R/zzz.R index e6315fc60..68d59245f 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -51,6 +51,8 @@ cmdstanr_initialize <- function() { call. = FALSE ) .cmdstanr$PATH <- NULL + .cmdstanr$VERSION <- NULL + .cmdstanr$WSL <- FALSE } else { set_cmdstan_path(path) } @@ -62,6 +64,8 @@ cmdstanr_initialize <- function() { call. = FALSE ) .cmdstanr$PATH <- NULL + .cmdstanr$VERSION <- NULL + .cmdstanr$WSL <- FALSE } } else { # environment variable not found diff --git a/tests/testthat/test-install.R b/tests/testthat/test-install.R index 28334ce68..da6d572f9 100644 --- a/tests/testthat/test-install.R +++ b/tests/testthat/test-install.R @@ -306,3 +306,118 @@ test_that("deprecated CMDSTANR_USE_MSYS_TOOLCHAIN is ignored with warning", { expect_silent(make_cmd()) }) }) + +test_that("rtools4x_toolchain_path prefers static-posix when available", { + skip_if(arch_is_aarch64()) + env_var <- paste0( + "RTOOLS", rtools4x_version(), + if (arch_is_aarch64()) "_AARCH64" else "", + "_HOME" + ) + fake_rtools_home <- tempfile(pattern = "rtools-home-pref-", tmpdir = tempdir(check = TRUE)) + on.exit(unlink(fake_rtools_home, recursive = TRUE), add = TRUE) + dir.create(file.path(fake_rtools_home, "x86_64-w64-mingw32.static.posix", "bin"), + recursive = TRUE, showWarnings = FALSE) + dir.create(file.path(fake_rtools_home, "mingw64", "bin"), + recursive = TRUE, showWarnings = FALSE) + file.create(file.path(fake_rtools_home, "x86_64-w64-mingw32.static.posix", "bin", "g++.exe")) + file.create(file.path(fake_rtools_home, "mingw64", "bin", "g++.exe")) + + withr::with_envvar(setNames(fake_rtools_home, env_var), { + expect_equal( + rtools4x_toolchain_path(), + repair_path(file.path(fake_rtools_home, "x86_64-w64-mingw32.static.posix", "bin")) + ) + }) +}) + +test_that("rtools4x_toolchain_path falls back to mingw64 for legacy layouts", { + skip_if(arch_is_aarch64()) + env_var <- paste0( + "RTOOLS", rtools4x_version(), + if (arch_is_aarch64()) "_AARCH64" else "", + "_HOME" + ) + fake_rtools_home <- tempfile(pattern = "rtools-home-fallback-", tmpdir = tempdir(check = TRUE)) + on.exit(unlink(fake_rtools_home, recursive = TRUE), add = TRUE) + dir.create(file.path(fake_rtools_home, "mingw64", "bin"), + recursive = TRUE, showWarnings = FALSE) + file.create(file.path(fake_rtools_home, "mingw64", "bin", "g++.exe")) + + withr::with_envvar(setNames(fake_rtools_home, env_var), { + expect_equal( + rtools4x_toolchain_path(), + repair_path(file.path(fake_rtools_home, "mingw64", "bin")) + ) + }) +}) + +test_that("rtools4x_toolchain_path prefers ABI-compatible legacy fallback", { + skip_if(arch_is_aarch64()) + env_var <- paste0( + "RTOOLS", rtools4x_version(), + if (arch_is_aarch64()) "_AARCH64" else "", + "_HOME" + ) + fake_rtools_home <- tempfile(pattern = "rtools-home-abi-", tmpdir = tempdir(check = TRUE)) + on.exit(unlink(fake_rtools_home, recursive = TRUE), add = TRUE) + dir.create(file.path(fake_rtools_home, "mingw64", "bin"), + recursive = TRUE, showWarnings = FALSE) + dir.create(file.path(fake_rtools_home, "ucrt64", "bin"), + recursive = TRUE, showWarnings = FALSE) + file.create(file.path(fake_rtools_home, "mingw64", "bin", "g++.exe")) + file.create(file.path(fake_rtools_home, "ucrt64", "bin", "g++.exe")) + + withr::with_envvar(setNames(fake_rtools_home, env_var), { + with_mocked_bindings( + { + expect_equal( + rtools4x_toolchain_path(), + repair_path(file.path(fake_rtools_home, "mingw64", "bin")) + ) + }, + is_ucrt_toolchain = function() FALSE + ) + with_mocked_bindings( + { + expect_equal( + rtools4x_toolchain_path(), + repair_path(file.path(fake_rtools_home, "ucrt64", "bin")) + ) + }, + is_ucrt_toolchain = function() TRUE + ) + }) +}) + +test_that("check_rtools4x_windows_toolchain reports checked toolchain paths", { + env_var <- paste0( + "RTOOLS", rtools4x_version(), + if (arch_is_aarch64()) "_AARCH64" else "", + "_HOME" + ) + fake_rtools_home <- tempfile(pattern = "rtools-home-invalid-", tmpdir = tempdir(check = TRUE)) + on.exit(unlink(fake_rtools_home, recursive = TRUE), add = TRUE) + dir.create(file.path(fake_rtools_home, "usr", "bin"), + recursive = TRUE, showWarnings = FALSE) + file.create(file.path(fake_rtools_home, "usr", "bin", "make.exe")) + if (arch_is_aarch64()) { + dir.create(file.path(fake_rtools_home, "aarch64-w64-mingw32.static.posix", "bin"), + recursive = TRUE, showWarnings = FALSE) + } else { + dir.create(file.path(fake_rtools_home, "x86_64-w64-mingw32.static.posix", "bin"), + recursive = TRUE, showWarnings = FALSE) + dir.create(file.path(fake_rtools_home, "ucrt64", "bin"), + recursive = TRUE, showWarnings = FALSE) + dir.create(file.path(fake_rtools_home, "mingw64", "bin"), + recursive = TRUE, showWarnings = FALSE) + } + + withr::with_envvar(setNames(fake_rtools_home, env_var), { + expect_error( + check_rtools4x_windows_toolchain(), + "Checked the following paths:", + fixed = TRUE + ) + }) +}) diff --git a/tests/testthat/test-path.R b/tests/testthat/test-path.R index 7aa031b1a..575d43d10 100644 --- a/tests/testthat/test-path.R +++ b/tests/testthat/test-path.R @@ -27,11 +27,16 @@ test_that("Setting bad path leads to warning (can't find directory)", { test_that("Setting bad path from env leads to warning (can't find directory)", { unset_cmdstan_path() + .cmdstanr$WSL <- TRUE Sys.setenv(CMDSTAN = "BAD_PATH") expect_warning( cmdstanr_initialize(), "Can't find directory specified by environment variable" ) + expect_null(.cmdstanr$PATH) + expect_null(.cmdstanr$VERSION) + expect_false(isTRUE(.cmdstanr$WSL)) + Sys.unsetenv("CMDSTAN") }) test_that("Setting path from env var is detected", { @@ -46,6 +51,7 @@ test_that("Setting path from env var is detected", { test_that("Unsupported CmdStan path from env var is rejected", { unset_cmdstan_path() + .cmdstanr$WSL <- TRUE parent_dir <- file.path(tempdir(check = TRUE), "cmdstan-env-parent") old_install <- file.path(parent_dir, "cmdstan-2.34.0") dir.create(old_install, recursive = TRUE, showWarnings = FALSE) @@ -60,6 +66,7 @@ test_that("Unsupported CmdStan path from env var is rejected", { if (!is.null(.cmdstanr$VERSION)) { expect_true(is_supported_cmdstan_version(.cmdstanr$VERSION)) } + expect_false(isTRUE(.cmdstanr$WSL)) }) test_that("cmdstanr_initialize() also looks for default path", { @@ -111,9 +118,11 @@ test_that("Warning message is thrown if can't detect version number", { test_that("Setting path rejects unsupported CmdStan versions", { old_path <- .cmdstanr$PATH old_version <- .cmdstanr$VERSION + old_wsl <- .cmdstanr$WSL on.exit({ .cmdstanr$PATH <- old_path .cmdstanr$VERSION <- old_version + .cmdstanr$WSL <- old_wsl }) path <- file.path(tempdir(check = TRUE), "cmdstan-2.34.0") @@ -128,6 +137,17 @@ test_that("Setting path rejects unsupported CmdStan versions", { ) expect_null(.cmdstanr$PATH) expect_null(.cmdstanr$VERSION) + expect_false(isTRUE(.cmdstanr$WSL)) +}) + +test_that("unset_cmdstan_path() also resets WSL state", { + .cmdstanr$PATH <- PATH + .cmdstanr$VERSION <- VERSION + .cmdstanr$WSL <- TRUE + unset_cmdstan_path() + expect_null(.cmdstanr$PATH) + expect_null(.cmdstanr$VERSION) + expect_false(isTRUE(.cmdstanr$WSL)) }) test_that("cmdstan_ext() works", { From 52c8abf5f2cdf5338782573502ec62a2c320d8e9 Mon Sep 17 00:00:00 2001 From: jgabry Date: Wed, 25 Feb 2026 16:33:13 -0700 Subject: [PATCH 07/15] Add some code comments --- R/install.R | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/R/install.R b/R/install.R index d42508b41..1e03e6c02 100644 --- a/R/install.R +++ b/R/install.R @@ -237,15 +237,6 @@ install_cmdstan <- function(dir = NULL, ) } - # Building fails on Apple silicon with < v2.31 due to a makefiles setting - # for stanc3, so manually implement the patch if needed from: - # https://github.com/stan-dev/cmdstan/pull/1127 - stanc_makefile <- readLines(file.path(dir_cmdstan, "make", "stanc")) - stanc_makefile <- gsub("\\bxattr -d com.apple.quarantine bin/stanc", - "-xattr -d com.apple.quarantine bin/stanc", - stanc_makefile) - writeLines(stanc_makefile, con = file.path(dir_cmdstan, "make", "stanc")) - if (is_ucrt_toolchain() && !wsl) { cmdstan_make_local( dir = dir_cmdstan, @@ -625,6 +616,7 @@ check_rtools4x_windows_toolchain <- function(fix = FALSE, quiet = FALSE) { ) } usr_bin <- repair_path(file.path(rtools_path, "usr", "bin")) + # Fail early with a clear message if the base make tool is missing make_found <- any(file.exists(file.path(usr_bin, c("make.exe", "mingw32-make.exe")))) if (!make_found) { stop( @@ -634,6 +626,7 @@ check_rtools4x_windows_toolchain <- function(fix = FALSE, quiet = FALSE) { ) } candidates <- rtools4x_toolchain_candidates() + # Validate candidate toolchains here so build errors later are not opaque has_usable_toolchain <- any(vapply(candidates, is_rtools4x_toolchain_usable, logical(1))) if (!has_usable_toolchain) { if (length(candidates) == 0) { @@ -745,11 +738,20 @@ toolchain_PATH_env_var <- function() { ) } +#' Ordered candidate RTools toolchain bin paths +#' +#' On x86_64, candidate order is ABI-aware so legacy fallback paths are tried +#' in an order compatible with the current R toolchain. +#' +#' @noRd +#' @return A character vector of normalized candidate toolchain bin paths rtools4x_toolchain_candidates <- function() { rtools_home <- rtools4x_home_path() if (!nzchar(rtools_home)) { return(character()) } + # Prefer the modern static toolchain first, then ABI-compatible legacy + # fallbacks for older Rtools layouts toolchains <- if (arch_is_aarch64()) { "aarch64-w64-mingw32.static.posix" } else if (is_ucrt_toolchain()) { @@ -760,6 +762,7 @@ rtools4x_toolchain_candidates <- function() { repair_path(file.path(rtools_home, toolchains, "bin")) } +# A candidate is usable if the directory exists and contains a g++ executable is_rtools4x_toolchain_usable <- function(path) { if (!nzchar(path) || !dir.exists(path)) { return(FALSE) @@ -767,11 +770,19 @@ is_rtools4x_toolchain_usable <- function(path) { any(file.exists(file.path(path, c("g++.exe", "g++")))) } +#' Resolve the preferred RTools toolchain bin path +#' +#' Returns the first usable path from `rtools4x_toolchain_candidates()`. If no +#' candidate is usable, returns the first candidate for deterministic diagnostics. +#' +#' @noRd +#' @return A single path string, or `""` if no candidates are available. rtools4x_toolchain_path <- function() { candidates <- rtools4x_toolchain_candidates() if (length(candidates) == 0) { return("") } + # Return the first usable candidate (ordered by preference above). usable <- vapply(candidates, is_rtools4x_toolchain_usable, logical(1)) if (any(usable)) { return(candidates[which(usable)[1]]) From 0d4cb1f96ca78e80b774de281d74d7f4817b5561 Mon Sep 17 00:00:00 2001 From: jgabry Date: Wed, 25 Feb 2026 16:35:49 -0700 Subject: [PATCH 08/15] Regenerate .Rd file --- R/install.R | 4 ++-- man/install_cmdstan.Rd | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/R/install.R b/R/install.R index 1e03e6c02..3c58110f2 100644 --- a/R/install.R +++ b/R/install.R @@ -22,8 +22,8 @@ #' C++ toolchain. It is called internally by `install_cmdstan()` but can also #' be called directly by the user. #' -#' CmdStan versions older than 2.35.0 are no longer supported. If you need to -#' work with an older CmdStan version we recommend installing an older +#' **CmdStan versions older than 2.35.0 are no longer supported.** If you need +#' to work with an older CmdStan version we recommend installing an older #' CmdStanR release from GitHub. #' #' @export diff --git a/man/install_cmdstan.Rd b/man/install_cmdstan.Rd index 54b7d41c5..fa4157071 100644 --- a/man/install_cmdstan.Rd +++ b/man/install_cmdstan.Rd @@ -119,8 +119,8 @@ The \code{check_cmdstan_toolchain()} function attempts to check for the required C++ toolchain. It is called internally by \code{install_cmdstan()} but can also be called directly by the user. -CmdStan versions older than 2.35.0 are no longer supported. If you need to -work with an older CmdStan version we recommend installing an older +\strong{CmdStan versions older than 2.35.0 are no longer supported.} If you need +to work with an older CmdStan version we recommend installing an older CmdStanR release from GitHub. } \examples{ From 3789dd690c5701173bf60e8a5d9b62a1a0ec7226 Mon Sep 17 00:00:00 2001 From: jgabry Date: Wed, 25 Feb 2026 16:43:54 -0700 Subject: [PATCH 09/15] Fix R version logging in Windows CI debug step --- .github/workflows/R-CMD-check.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index a4a46ecbe..523d29f84 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -81,7 +81,7 @@ jobs: rtools_home <- cmdstanr:::rtools4x_home_path() candidates <- cmdstanr:::rtools4x_toolchain_candidates() selected <- cmdstanr:::rtools4x_toolchain_path() - cat("R version: ", getRversion(), "\n", sep = "") + cat("R version: ", as.character(getRversion()), "\n", sep = "") cat("Rtools home: ", rtools_home, "\n", sep = "") cat("Toolchain candidates:\n") if (length(candidates) == 0) { From f7990471ff56a17b1f6ab45774eb4100bf3ae3db Mon Sep 17 00:00:00 2001 From: jgabry Date: Thu, 26 Feb 2026 10:20:01 -0700 Subject: [PATCH 10/15] Fix WSL path scoping and Windows CI edge-case tests - Scope cmdstan_default_path(dir=...) to the provided directory and avoid unrelated WSL fallback.\n- Keep Windows toolchain debug validation aligned with is_rtools4x_toolchain_usable().\n- Add a windows-2022 (R 4.1) CI-only skip flag for pareto_smooth-dependent init tests.\n- Centralize that skip logic in a shared test helper and apply it to all affected tests.\n- Harden recursive make test subprocess env and Makefile quoting for Windows reliability. --- .github/workflows/R-CMD-check.yaml | 5 ++--- R/path.R | 20 +++++++++++--------- tests/testthat/helper-envvars-and-paths.R | 6 ++++++ tests/testthat/test-fit-init.R | 6 +++++- tests/testthat/test-model-init.R | 1 + tests/testthat/test-utils.R | 7 ++++++- tests/testthat/testdata/Makefile | 2 +- 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 523d29f84..0d6d1ef09 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -47,6 +47,7 @@ jobs: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} NOT_CRAN: true CMDSTANR_OPENCL_TESTS: ${{ matrix.config.opencl }} + CMDSTANR_SKIP_PARETO_SMOOTH_INIT_TESTS: ${{ matrix.config.os == 'windows-2022' && matrix.config.r == '4.1' }} CMDSTAN_TEST_TARBALL_URL: ${{ github.event.inputs.tarball_url }} PKG_SYSREQS_DB_UPDATE_TIMEOUT: 30s @@ -91,9 +92,7 @@ jobs: cat("\n") } cat("Selected toolchain: ", selected, "\n", sep = "") - selected_ok <- nzchar(selected) && - dir.exists(selected) && - any(file.exists(file.path(selected, c("g++.exe", "g++")))) + selected_ok <- cmdstanr:::is_rtools4x_toolchain_usable(selected) if (!selected_ok) { stop("Resolved Windows toolchain path is not usable.") } diff --git a/R/path.R b/R/path.R index fdd9adcf0..94acda56b 100644 --- a/R/path.R +++ b/R/path.R @@ -163,18 +163,20 @@ cmdstan_default_install_path <- function(wsl = FALSE) { cmdstan_default_path <- function(dir = NULL) { if (!is.null(dir)) { installs_path <- dir - } else { - installs_path <- cmdstan_default_install_path() - } - wsl_installed <- wsl_installed() - if (!isTRUE(wsl_installed)) { wsl_installs_path <- NULL wsl_path_exists <- FALSE } else { - wsl_installs_path <- cmdstan_default_install_path(wsl = TRUE) - wsl_path_linux <- gsub(wsl_dir_prefix(wsl = TRUE), "", wsl_installs_path, - fixed=TRUE) - wsl_path_exists <- isTRUE(.wsl_check_exists(wsl_path_linux)) + installs_path <- cmdstan_default_install_path() + wsl_installed <- wsl_installed() + if (!isTRUE(wsl_installed)) { + wsl_installs_path <- NULL + wsl_path_exists <- FALSE + } else { + wsl_installs_path <- cmdstan_default_install_path(wsl = TRUE) + wsl_path_linux <- gsub(wsl_dir_prefix(wsl = TRUE), "", wsl_installs_path, + fixed=TRUE) + wsl_path_exists <- isTRUE(.wsl_check_exists(wsl_path_linux)) + } } if (dir.exists(installs_path) || wsl_path_exists) { latest_cmdstan <- ifelse(dir.exists(installs_path), diff --git a/tests/testthat/helper-envvars-and-paths.R b/tests/testthat/helper-envvars-and-paths.R index 66283716b..1ae32b8db 100644 --- a/tests/testthat/helper-envvars-and-paths.R +++ b/tests/testthat/helper-envvars-and-paths.R @@ -23,3 +23,9 @@ delete_extensions <- function() { c("", ".o",".hpp") } } + +skip_if_legacy_win41_pareto <- function() { + if (tolower(Sys.getenv("CMDSTANR_SKIP_PARETO_SMOOTH_INIT_TESTS")) %in% c("1", "true")) { + skip("Skipping tests requiring posterior::pareto_smooth on windows-2022 R 4.1 CI.") + } +} diff --git a/tests/testthat/test-fit-init.R b/tests/testthat/test-fit-init.R index 57d8984b0..cb670d7f7 100644 --- a/tests/testthat/test-fit-init.R +++ b/tests/testthat/test-fit-init.R @@ -1,7 +1,6 @@ context("fitted-inits") set_cmdstan_path() - data_list_schools <- testing_data("schools") data_list_logistic <- testing_data("logistic") test_inits <- function(mod, fit_init, data_list = NULL) { @@ -50,6 +49,7 @@ test_that("Subsets of parameters are allowed", { }) test_that("Pathfinder works as init", { + skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_path_init <- mod_logistic$pathfinder( seed=1234, data = data_list_logistic, refresh = 0, num_paths = 1)) @@ -57,6 +57,7 @@ test_that("Pathfinder works as init", { }) test_that("Multi Pathfinder method works as init", { + skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_path_init <- mod_logistic$pathfinder(seed=1234, data = data_list_logistic, refresh = 0, num_paths = 4)) @@ -64,6 +65,7 @@ test_that("Multi Pathfinder method works as init", { }) test_that("Pathfinder method with psis_resample as false works as init", { + skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_path_init <- mod_logistic$pathfinder( seed=1234, data = data_list_logistic, refresh = 0, num_paths = 1, @@ -73,6 +75,7 @@ test_that("Pathfinder method with psis_resample as false works as init", { test_that("Multi Pathfinder method with psis_resample as false works as init", { + skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_path_init <- mod_logistic$pathfinder( seed=1234, data = data_list_logistic, refresh = 0, num_paths = 4, @@ -98,6 +101,7 @@ test_that("Multi Pathfinder method with calculate_lp as false works as init", { }) test_that("Variational method works as init", { + skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_vb_init <- mod_logistic$variational( data = data_list_logistic, seed=1234, refresh = 0)) diff --git a/tests/testthat/test-model-init.R b/tests/testthat/test-model-init.R index ebe546777..b03e56563 100644 --- a/tests/testthat/test-model-init.R +++ b/tests/testthat/test-model-init.R @@ -314,6 +314,7 @@ test_that("Initial values for single-element containers treated correctly", { }) test_that("Pathfinder inits do not drop dimensions", { + skip_if_legacy_win41_pareto() modcode <- " data { int N; diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 7389e4886..677b338e5 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -258,8 +258,13 @@ test_that("as_mcmc.list() works", { test_that("get_cmdstan_flags() can be used recursively in `make`", { mkfile <- normalizePath(test_path("testdata", "Makefile")) nonrecursive_flags <- get_cmdstan_flags("STANCFLAGS") + child_env <- c( + R_HOME = R.home(), + R_LIBS = paste(.libPaths(), collapse = .Platform$path.sep), + R_LIBS_USER = Sys.getenv("R_LIBS_USER") + ) stdo <- processx::run( - command = "make", args = sprintf("--file=%s", mkfile) + command = "make", args = sprintf("--file=%s", mkfile), env = child_env )$stdout recursive_flags <- readLines(textConnection(stdo)) expect_equal(nonrecursive_flags, recursive_flags) diff --git a/tests/testthat/testdata/Makefile b/tests/testthat/testdata/Makefile index 4a58d6544..76af17e77 100644 --- a/tests/testthat/testdata/Makefile +++ b/tests/testthat/testdata/Makefile @@ -1,3 +1,3 @@ test: - @$(R_HOME)/bin/Rscript --vanilla -e 'cat(cmdstanr:::get_cmdstan_flags("STANCFLAGS"))' + @$(R_HOME)/bin/Rscript --vanilla -e "cat(cmdstanr:::get_cmdstan_flags('STANCFLAGS'))" From 819d479c2e5cae0eb6981c1a8fb2a9e381f96eaf Mon Sep 17 00:00:00 2001 From: jgabry Date: Thu, 26 Feb 2026 12:41:30 -0700 Subject: [PATCH 11/15] Preserve env for recursive make test --- tests/testthat/test-utils.R | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 677b338e5..cb00d9043 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -258,11 +258,10 @@ test_that("as_mcmc.list() works", { test_that("get_cmdstan_flags() can be used recursively in `make`", { mkfile <- normalizePath(test_path("testdata", "Makefile")) nonrecursive_flags <- get_cmdstan_flags("STANCFLAGS") - child_env <- c( - R_HOME = R.home(), - R_LIBS = paste(.libPaths(), collapse = .Platform$path.sep), - R_LIBS_USER = Sys.getenv("R_LIBS_USER") - ) + child_env <- Sys.getenv() + child_env["R_HOME"] <- R.home() + child_env["R_LIBS"] <- paste(.libPaths(), collapse = .Platform$path.sep) + child_env["R_LIBS_USER"] <- Sys.getenv("R_LIBS_USER") stdo <- processx::run( command = "make", args = sprintf("--file=%s", mkfile), env = child_env )$stdout From 6a4ed9388036a40d1b719fcaac136586018114b6 Mon Sep 17 00:00:00 2001 From: jgabry Date: Thu, 26 Feb 2026 13:04:09 -0700 Subject: [PATCH 12/15] Add diagnostics to recursive make test --- tests/testthat/test-utils.R | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index cb00d9043..7096bb8cf 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -258,13 +258,26 @@ test_that("as_mcmc.list() works", { test_that("get_cmdstan_flags() can be used recursively in `make`", { mkfile <- normalizePath(test_path("testdata", "Makefile")) nonrecursive_flags <- get_cmdstan_flags("STANCFLAGS") - child_env <- Sys.getenv() - child_env["R_HOME"] <- R.home() - child_env["R_LIBS"] <- paste(.libPaths(), collapse = .Platform$path.sep) - child_env["R_LIBS_USER"] <- Sys.getenv("R_LIBS_USER") - stdo <- processx::run( - command = "make", args = sprintf("--file=%s", mkfile), env = child_env - )$stdout + recursive_run <- processx::run( + command = "make", + args = sprintf("--file=%s", mkfile), + error_on_status = FALSE + ) + if (recursive_run$status != 0) { + fail( + paste( + "Recursive make failed.", + paste0("status: ", recursive_run$status), + "stdout:", + recursive_run$stdout, + "stderr:", + recursive_run$stderr, + sep = "\n" + ) + ) + return(invisible()) + } + stdo <- recursive_run$stdout recursive_flags <- readLines(textConnection(stdo)) expect_equal(nonrecursive_flags, recursive_flags) }) From 4a8c1a1f78369767e074c9196959d0c3a3c4ab53 Mon Sep 17 00:00:00 2001 From: jgabry Date: Thu, 26 Feb 2026 14:39:54 -0700 Subject: [PATCH 13/15] bump required version of posterior to 1.5.0 we need `posterior::pareto_smooth()` --- DESCRIPTION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/DESCRIPTION b/DESCRIPTION index 263357192..b49e9ab82 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -41,7 +41,7 @@ Imports: checkmate, data.table, jsonlite (>= 1.2.0), - posterior (>= 1.4.1), + posterior (>= 1.5.0), processx (>= 3.5.0), R6 (>= 2.4.0), withr (>= 2.5.0), From a428a2fd8096ba09e57acacccdc8e124cc566f2a Mon Sep 17 00:00:00 2001 From: jgabry Date: Thu, 26 Feb 2026 14:55:56 -0700 Subject: [PATCH 14/15] Move recursive make fixture to test resources --- .../{testdata/Makefile => resources/recursive-cmdstan-flags.mk} | 0 tests/testthat/test-utils.R | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename tests/testthat/{testdata/Makefile => resources/recursive-cmdstan-flags.mk} (100%) diff --git a/tests/testthat/testdata/Makefile b/tests/testthat/resources/recursive-cmdstan-flags.mk similarity index 100% rename from tests/testthat/testdata/Makefile rename to tests/testthat/resources/recursive-cmdstan-flags.mk diff --git a/tests/testthat/test-utils.R b/tests/testthat/test-utils.R index 7096bb8cf..9b35dde4f 100644 --- a/tests/testthat/test-utils.R +++ b/tests/testthat/test-utils.R @@ -256,7 +256,7 @@ test_that("as_mcmc.list() works", { }) test_that("get_cmdstan_flags() can be used recursively in `make`", { - mkfile <- normalizePath(test_path("testdata", "Makefile")) + mkfile <- normalizePath(test_path("resources", "recursive-cmdstan-flags.mk")) nonrecursive_flags <- get_cmdstan_flags("STANCFLAGS") recursive_run <- processx::run( command = "make", From 9c579c364ce510959efabdf9852c11ac24625ad0 Mon Sep 17 00:00:00 2001 From: jgabry Date: Fri, 27 Feb 2026 11:47:57 -0700 Subject: [PATCH 15/15] Remove legacy windows pareto test skips --- .github/workflows/R-CMD-check.yaml | 1 - tests/testthat/helper-envvars-and-paths.R | 6 ------ tests/testthat/test-fit-init.R | 5 ----- tests/testthat/test-model-init.R | 1 - 4 files changed, 13 deletions(-) diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index 0d6d1ef09..d2bd9982a 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -47,7 +47,6 @@ jobs: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} NOT_CRAN: true CMDSTANR_OPENCL_TESTS: ${{ matrix.config.opencl }} - CMDSTANR_SKIP_PARETO_SMOOTH_INIT_TESTS: ${{ matrix.config.os == 'windows-2022' && matrix.config.r == '4.1' }} CMDSTAN_TEST_TARBALL_URL: ${{ github.event.inputs.tarball_url }} PKG_SYSREQS_DB_UPDATE_TIMEOUT: 30s diff --git a/tests/testthat/helper-envvars-and-paths.R b/tests/testthat/helper-envvars-and-paths.R index 1ae32b8db..66283716b 100644 --- a/tests/testthat/helper-envvars-and-paths.R +++ b/tests/testthat/helper-envvars-and-paths.R @@ -23,9 +23,3 @@ delete_extensions <- function() { c("", ".o",".hpp") } } - -skip_if_legacy_win41_pareto <- function() { - if (tolower(Sys.getenv("CMDSTANR_SKIP_PARETO_SMOOTH_INIT_TESTS")) %in% c("1", "true")) { - skip("Skipping tests requiring posterior::pareto_smooth on windows-2022 R 4.1 CI.") - } -} diff --git a/tests/testthat/test-fit-init.R b/tests/testthat/test-fit-init.R index cb670d7f7..7b78c73a2 100644 --- a/tests/testthat/test-fit-init.R +++ b/tests/testthat/test-fit-init.R @@ -49,7 +49,6 @@ test_that("Subsets of parameters are allowed", { }) test_that("Pathfinder works as init", { - skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_path_init <- mod_logistic$pathfinder( seed=1234, data = data_list_logistic, refresh = 0, num_paths = 1)) @@ -57,7 +56,6 @@ test_that("Pathfinder works as init", { }) test_that("Multi Pathfinder method works as init", { - skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_path_init <- mod_logistic$pathfinder(seed=1234, data = data_list_logistic, refresh = 0, num_paths = 4)) @@ -65,7 +63,6 @@ test_that("Multi Pathfinder method works as init", { }) test_that("Pathfinder method with psis_resample as false works as init", { - skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_path_init <- mod_logistic$pathfinder( seed=1234, data = data_list_logistic, refresh = 0, num_paths = 1, @@ -75,7 +72,6 @@ test_that("Pathfinder method with psis_resample as false works as init", { test_that("Multi Pathfinder method with psis_resample as false works as init", { - skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_path_init <- mod_logistic$pathfinder( seed=1234, data = data_list_logistic, refresh = 0, num_paths = 4, @@ -101,7 +97,6 @@ test_that("Multi Pathfinder method with calculate_lp as false works as init", { }) test_that("Variational method works as init", { - skip_if_legacy_win41_pareto() mod_logistic <- testing_model("logistic") utils::capture.output(fit_vb_init <- mod_logistic$variational( data = data_list_logistic, seed=1234, refresh = 0)) diff --git a/tests/testthat/test-model-init.R b/tests/testthat/test-model-init.R index b03e56563..ebe546777 100644 --- a/tests/testthat/test-model-init.R +++ b/tests/testthat/test-model-init.R @@ -314,7 +314,6 @@ test_that("Initial values for single-element containers treated correctly", { }) test_that("Pathfinder inits do not drop dimensions", { - skip_if_legacy_win41_pareto() modcode <- " data { int N;