Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 30 additions & 21 deletions .github/workflows/R-CMD-check.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,6 @@ jobs:
- name: cmdstan env vars
run: |
echo "CMDSTAN_PATH=${HOME}/.cmdstan" >> $GITHUB_ENV
# GHA Windows ARM containers configured with RTOOLS45_HOME instead of RTOOLS45_AARCH64_HOME
if [[ "${{ matrix.config.os }}" == "windows-11-arm" ]]; then
echo "RTOOLS45_AARCH64_HOME=C:/rtools45-aarch64" >> $GITHUB_ENV
fi
shell: bash

- uses: actions/checkout@v7
Expand All @@ -82,25 +78,38 @@ jobs:
cache: "always"
extra-packages: any::rcmdcheck, local::., ${{ matrix.config.r == '4.1' && 'qs2=?ignore' || '' }}

- name: Debug Windows toolchain resolution
if: ${{ runner.os == 'Windows' }}
- name: Test Windows toolchain in directory with spaces
if: ${{ runner.os == 'Windows' && matrix.config.r == '4.1' }}
run: |
rtools_home <- cmdstanr:::rtools4x_home_path()
candidates <- cmdstanr:::rtools4x_toolchain_candidates()
selected <- cmdstanr:::rtools4x_toolchain_path()
cat("R version: ", as.character(getRversion()), "\n", sep = "")
cat("Rtools home: ", rtools_home, "\n", sep = "")
cat("Toolchain candidates:\n")
if (length(candidates) == 0) {
cat(" - <none>\n")
} else {
cat(paste0(" - ", candidates), sep = "\n")
cat("\n")
New-Item -ItemType Directory -Path "C:\tmp path with spaces" -Force
Move-Item -Path "C:\rtools40" -Destination "C:\tmp path with spaces\"
echo "RTOOLS40_HOME=C:/tmp path with spaces/rtools40" >> $env:GITHUB_ENV
shell: pwsh

- name: Verify R_TOOLS_SOFT configuration
if: ${{ matrix.config.os == 'windows-latest' && matrix.config.r != '4.1' }}
run: |
rtools_soft <- suppressWarnings(
tools::Rcmd(c("config", "R_TOOLS_SOFT"), stdout = TRUE)
)
if (
!is.null(attr(rtools_soft, "status")) ||
length(rtools_soft) != 1L ||
!nzchar(trimws(rtools_soft))
) {
stop("R CMD config R_TOOLS_SOFT did not return a valid path.")
}
cat("Selected toolchain: ", selected, "\n", sep = "")
selected_ok <- cmdstanr:::is_rtools4x_toolchain_usable(selected)
if (!selected_ok) {
stop("Resolved Windows toolchain path is not usable.")
rtools_soft <- trimws(rtools_soft)
required <- c(
file.path(dirname(rtools_soft), "usr", "bin", "make.exe"),
file.path(rtools_soft, "bin", "c++.exe")
)
missing <- required[!file.exists(required)]
if (length(missing)) {
stop(
"R_TOOLS_SOFT is configured, but expected executables are missing: ",
paste(missing, collapse = ", ")
)
}
shell: Rscript {0}

Expand Down
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: cmdstanr
Title: R Interface to 'CmdStan'
Version: 0.9.0.9001
Version: 0.9.0.9002
Date: 2025-03-30
Authors@R:
c(person(given = "Jonah", family = "Gabry", role = c("aut", "cre"),
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ variables are, instead of erroring. (#1225)
which previously errored. (#1225)
* The `CMDSTANR_NO_VER_CHECK` R option and environment variable are deprecated
as of CmdStanR 1.0.0; use the lowercase `cmdstanr_no_ver_check` forms instead.
* `check_cmdstan_toolchain()` now locates Windows toolchains using `R_TOOLS_SOFT` and falls back to `PATH`, improving support for alternate R distributions and future Rtools releases. (#1211)
* `$compile()` now works with named `stanc_options` values such as
`canonicalize`. The values were shell-quoted for Make and the same quoted
strings were also passed to `stanc` directly, which rejected them. (#1227)
Expand Down
198 changes: 60 additions & 138 deletions R/install.R
Original file line number Diff line number Diff line change
Expand Up @@ -616,54 +616,13 @@ check_wsl_toolchain <- function() {
}

check_rtools4x_windows_toolchain <- function(quiet = FALSE) {
rtools_path <- rtools4x_home_path()
rtools_version <- paste0("Rtools", rtools4x_version())
# 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)) {
toolchain_path <- toolchain_PATH_env_var()
if (is.null(toolchain_path)) {
stop(
"\n", rtools_version, " was not found but is required to run CmdStan with R version ",
R.version$major, ".", R.version$minor, ".",
"\nPlease install or reinstall the appropriate Rtools version for this R installation,",
"\nrestart R, and then run cmdstanr::check_cmdstan_toolchain().",
call. = FALSE
)
}
# If RTools is installed in a path with spaces or brackets
# we error as this path is not valid
if (grepl("\\(|)| ", rtools_path)) {
stop(
"\n", rtools_version, " is installed in a path with spaces or brackets, which is not supported.",
Comment thread
jgabry marked this conversation as resolved.
"\nPlease reinstall the appropriate Rtools version for this R installation to a valid path,",
"\nrestart R, and then run cmdstanr::check_cmdstan_toolchain().",
call. = 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(
"\n", rtools_version, " is missing the required 'make' executable in ", usr_bin, ".",
"\nPlease reinstall the appropriate Rtools version for this R installation,",
"\nrestart R, and then run cmdstanr::check_cmdstan_toolchain().",
call. = 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) {
candidates_message <- "\n- <none>"
} 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 the appropriate Rtools version for this R installation,",
"CmdStanR could not find both make and a C++ compiler in R's ",
"configured toolchain or on PATH.",
"\nPlease install or reinstall the appropriate Rtools version for this ",
"R installation, or add a compatible toolchain to PATH,",
"\nrestart R, and then run cmdstanr::check_cmdstan_toolchain().",
call. = FALSE
)
Expand Down Expand Up @@ -749,108 +708,71 @@ cmdstan_arch_suffix <- function(version = NULL) {
paste0("-linux-", selected_arch)
}

toolchain_PATH_env_var <- function() {
if (!os_is_windows()) {
return(NULL)
}
rtools_home <- rtools4x_home_path()
if (!nzchar(rtools_home)) {
return(NULL)
}
paste0(
repair_path(file.path(rtools_home, "usr", "bin")), ";",
rtools4x_toolchain_path()
)
}
# Thin wrapper around `tools::Rcmd()` to allow mocking
.cmdstanr_rcmd <- function(...) tools::Rcmd(...)

#' 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()) {
c("x86_64-w64-mingw32.static.posix", "ucrt64", "mingw64")
toolchain_PATH_env_var <- function() {
# Return a previously successful lookup if available
# For non-windows systems the initialized path stays NULL
if (!is.null(.cmdstanr$TOOLCHAIN_PATH) || !os_is_windows()) {
return(.cmdstanr$TOOLCHAIN_PATH)
}

# Lookup the configured toolchain location for the installation
# This variable is set at installation since R 4.2
# e.g., 'C:/rtools45/x86_64-w64-mingw32.static.posix'
# R 4.0 and R 4.1 did not set the R_TOOLS_SOFT config variable, so
# we use the RTOOLS40_HOME environment variable instead
if (current_r_version() < "4.2.0") {
rtools40_home <- Sys.getenv("RTOOLS40_HOME", "C:\\rtools40")
r_arch <- ifelse(Sys.getenv("R_ARCH") == "/i386", "mingw32", "mingw64")
rtools_soft <- file.path(rtools40_home, r_arch)
} else {
c("x86_64-w64-mingw32.static.posix", "mingw64", "ucrt64")
rtools_soft <- tryCatch(
suppressWarnings(
.cmdstanr_rcmd(c("config", "R_TOOLS_SOFT"), stdout = TRUE)
),
error = function(e) ""
)
if (!is.null(attr(rtools_soft, "status")) || length(rtools_soft) != 1L) {
rtools_soft <- ""
} else {
rtools_soft <- trimws(rtools_soft)
}
}
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)
}
any(file.exists(file.path(path, c("g++.exe", "g++"))))
}
rtools_bin_dir <- file.path(dirname(rtools_soft), "usr", "bin")
rtools_cpp_dir <- file.path(rtools_soft, "bin")

#' 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("")
# R 4.2+ prepends the toolchain directory to PATH, so it will be found first
if (!nzchar(rtools_soft) ||
!file.exists(file.path(rtools_bin_dir, "make.exe"))) {
make_path <- Sys.which("make")
rtools_bin_dir <- ifelse(nzchar(make_path), dirname(make_path), "")
}
# 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]])
if (!nzchar(rtools_soft) ||
!file.exists(file.path(rtools_cpp_dir, "c++.exe"))) {
cpp_path <- Sys.which("c++")
rtools_cpp_dir <- ifelse(nzchar(cpp_path), dirname(cpp_path), "")
}
candidates[1]
}

rtools4x_version <- function() {
rtools_ver <- NULL
r_version <- current_r_version()

if (r_version < "4.2.0") {
rtools_ver <- "40"
} else if (r_version < "4.3.0") {
rtools_ver <- "42"
} else if (r_version < "4.4.0") {
rtools_ver <- "43"
} else if (r_version < "4.5.0") {
rtools_ver <- "44"
} else {
rtools_ver <- "45"
}
rtools_ver
}

rtools4x_home_path <- function() {
rtools_ver <- rtools4x_version()
if (arch_is_aarch64()) {
rtools_ver <- paste0(rtools_ver, "_AARCH64")
}
path <- Sys.getenv(paste0("RTOOLS", rtools_ver, "_HOME"))

if (!nzchar(path)) {
default_path <- repair_path(file.path(paste0("C:/rtools", rtools_ver)))
if (arch_is_aarch64()) {
default_path <- paste0(default_path, "-aarch64")
}
if (dir.exists(default_path)) {
path <- default_path
if (rtools_bin_dir != "" && rtools_cpp_dir != "") {
toolchain_dirs <- unique(
repair_path(short_path(c(rtools_bin_dir, rtools_cpp_dir)))
)
if (any(grepl("[() ]", toolchain_dirs))) {
stop(
"The Windows toolchain path contains spaces or parentheses, and ",
"CmdStanR could not convert it to a usable short path. Please install ",
"or move the toolchain to a path without spaces or parentheses, ",
"restart R, and then run cmdstanr::check_cmdstan_toolchain().",
call. = FALSE
)
}
.cmdstanr$TOOLCHAIN_PATH <- paste(toolchain_dirs, collapse = ";")
}

path
.cmdstanr$TOOLCHAIN_PATH
}

assert_supported_requested_cmdstan_version <- function(version, source = "version") {
Expand Down
1 change: 1 addition & 0 deletions R/path.R
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ cmdstan_version <- function(error_on_NA = TRUE) {
.cmdstanr$VERSION <- NULL
.cmdstanr$TEMP_DIR <- NULL
.cmdstanr$WSL <- FALSE
.cmdstanr$TOOLCHAIN_PATH <- NULL

unset_cmdstan_path <- function() {
.cmdstanr$PATH <- NULL
Expand Down
4 changes: 0 additions & 4 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,6 @@ is_rosetta2 <- function() {
rosetta2
}

arch_is_aarch64 <- function() {
isTRUE(R.version$arch == "aarch64")
}

# Returns the type of make command to use to compile depending on the OS
# First checks if $MAKE is set, otherwise falls back to "make"
make_cmd <- function() {
Expand Down
18 changes: 18 additions & 0 deletions tests/testthat/_snaps/install.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,21 @@
Warning:
The 'fix' argument is deprecated as of CmdStanR 1.0.0 and will be removed in a future release.

# toolchain_PATH_env_var() rejects unsafe toolchain paths

Code
toolchain_PATH_env_var()
Condition
Error:
! The Windows toolchain path contains spaces or parentheses, and CmdStanR could not convert it to a usable short path. Please install or move the toolchain to a path without spaces or parentheses, restart R, and then run cmdstanr::check_cmdstan_toolchain().

# check_rtools4x_windows_toolchain() stops when no toolchain found

Code
check_rtools4x_windows_toolchain()
Condition
Error:
! CmdStanR could not find both make and a C++ compiler in R's configured toolchain or on PATH.
Please install or reinstall the appropriate Rtools version for this R installation, or add a compatible toolchain to PATH,
restart R, and then run cmdstanr::check_cmdstan_toolchain().

Loading
Loading