Skip to content

Commit 9efd39a

Browse files
authored
Merge pull request #1226 from stan-dev/bugfix-issue-820
Fix include paths with spaces and resolve them at model creation
2 parents cd4bb05 + de41f16 commit 9efd39a

9 files changed

Lines changed: 210 additions & 48 deletions

File tree

NEWS.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ variables are, instead of erroring. (#1225)
1515
which previously errored. (#1225)
1616
* The `CMDSTANR_NO_VER_CHECK` R option and environment variable are deprecated
1717
as of CmdStanR 1.0.0; use the lowercase `cmdstanr_no_ver_check` forms instead.
18+
* CmdStanModel methods now correctly handle `#include` directories with spaces
19+
in their paths. (#820)
20+
* `$include_paths()` now returns absolute paths, and relative include paths are
21+
resolved when the model object is created or `$compile()` is called rather than
22+
on each `stanc` call. Previously a model created from a relative path could
23+
resolve `#include` directives against the wrong directory if the working
24+
directory changed. (#1229)
1825
* `$cpp_options()` no longer includes a `STAN_VERSION` entry read from the model
1926
executable's metadata. It was never a C++ option; use `$cmdstan_version()` instead. (#1215)
2027
* CmdStanModel methods now use executable metadata regardless of the

R/model.R

Lines changed: 42 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,7 @@ CmdStanModel <- R6::R6Class(
259259
if (!is.null(stan_file)) {
260260
assert_file_exists(stan_file, access = "r", extension = c("stan", "stanfunctions"))
261261
checkmate::assert_flag(compile)
262-
private$stan_file_ <- absolute_path(stan_file)
262+
private$stan_file_ <- resolve_path(stan_file)
263263
private$stan_code_ <- readLines(stan_file)
264264
private$model_name_ <- gsub(" ", "_", strip_ext(basename(private$stan_file_)))
265265
private$precompile_cpp_options_ <- args$cpp_options %||% list()
@@ -269,20 +269,20 @@ CmdStanModel <- R6::R6Class(
269269
private$using_user_header_ <- TRUE
270270
}
271271
if (is.null(args$include_paths) && any(grepl("#include" , private$stan_code_))) {
272-
private$precompile_include_paths_ <- dirname(stan_file)
272+
private$precompile_include_paths_ <- dirname(private$stan_file_)
273273
} else {
274-
private$precompile_include_paths_ <- args$include_paths
274+
private$precompile_include_paths_ <- resolve_path(args$include_paths)
275275
}
276276
}
277277
if (!is.null(exe_file)) {
278278
ext <- if (os_is_windows() && !os_is_wsl()) "exe" else ""
279-
private$exe_file_ <- repair_path(absolute_path(exe_file))
279+
private$exe_file_ <- resolve_path(exe_file)
280280
if (is.null(stan_file)) {
281281
assert_file_exists(private$exe_file_, access = "r", extension = ext)
282282
private$model_name_ <- gsub(" ", "_", strip_ext(basename(private$exe_file_)))
283283
}
284284
private$include_paths_ <-
285-
private$precompile_include_paths_ %||% args$include_paths
285+
private$precompile_include_paths_ %||% resolve_path(args$include_paths)
286286
}
287287
if (!is.null(stan_file) && compile) {
288288
self$compile(...)
@@ -422,7 +422,7 @@ CmdStanModel <- R6::R6Class(
422422
#' * `$model_name()` returns the model name as a string.
423423
#' * `$exe_file()` returns a path as a string, or `character(0)` if no
424424
#' executable path is set.
425-
#' * `$include_paths()` returns a character vector of paths or `NULL`.
425+
#' * `$include_paths()` returns a character vector of absolute paths or `NULL`.
426426
#' * `$cmdstan_version()` returns a CmdStan version as a string.
427427
#' * `$cpp_options()` returns a named list of C++ options.
428428
#' * `$hpp_file()` returns the path to the `.hpp` file as a string when C++ code
@@ -480,7 +480,10 @@ NULL
480480
#' [`$check_syntax()`][model-method-check_syntax] method can be used instead.
481481
#' @param include_paths (character vector) Paths to directories where Stan
482482
#' should look for files specified in `#include` directives in the Stan
483-
#' program.
483+
#' program. Relative paths are resolved against the working directory when
484+
#' the model object is created (or when `$compile()` is called) and stored as
485+
#' absolute paths, so subsequent changes to the working directory do not
486+
#' affect them.
484487
#' @param user_header (string) The path to a C++ file (with a .hpp extension)
485488
#' to compile with the Stan model.
486489
#' @param cpp_options (list) Any makefile options to be used when compiling the
@@ -594,7 +597,7 @@ compile <- function(quiet = TRUE,
594597
if (is.null(include_paths) && !is.null(private$precompile_include_paths_)) {
595598
include_paths <- private$precompile_include_paths_
596599
}
597-
private$include_paths_ <- include_paths
600+
private$include_paths_ <- resolve_path(include_paths)
598601
if (is.null(dir) && !is.null(private$dir_)) {
599602
dir <- absolute_path(private$dir_)
600603
} else if (!is.null(dir)) {
@@ -724,7 +727,7 @@ compile <- function(quiet = TRUE,
724727
if (length(stancflags_local) > 0) {
725728
stancflags_combined <- c(stancflags_combined, stancflags_local)
726729
}
727-
stanc_inc_paths <- include_paths_stanc3_args(include_paths, standalone_call = TRUE)
730+
stanc_inc_paths <- include_paths_stanc3_args(include_paths, direct_call = TRUE)
728731
stancflags_standalone <- c("--standalone-functions", stanc_inc_paths, stancflags_combined)
729732
self$functions$hpp_code <- get_standalone_hpp(temp_stan_file, stancflags_standalone)
730733
private$model_methods_env_ <- new.env()
@@ -980,7 +983,10 @@ check_syntax <- function(pedantic = FALSE,
980983
stanc_options[["warn-pedantic"]] <- TRUE
981984
}
982985

983-
stancflags_val <- include_paths_stanc3_args(include_paths)
986+
stancflags_val <- include_paths_stanc3_args(
987+
include_paths,
988+
direct_call = TRUE
989+
)
984990

985991
if (is.null(stanc_options[["name"]])) {
986992
stanc_options[["name"]] <- paste0(self$model_name(), "_model")
@@ -1106,7 +1112,10 @@ format <- function(overwrite_file = FALSE,
11061112
lower = 1, len = 1, null.ok = TRUE
11071113
)
11081114
stanc_options <- private$precompile_stanc_options_
1109-
stancflags_val <- include_paths_stanc3_args(self$include_paths())
1115+
stancflags_val <- include_paths_stanc3_args(
1116+
self$include_paths(),
1117+
direct_call = TRUE
1118+
)
11101119
stanc_options["auto-format"] <- TRUE
11111120
if (!is.null(max_line_length)) {
11121121
stanc_options["max-line-length"] <- max_line_length
@@ -2462,19 +2471,34 @@ assert_stan_file_exists <- function(stan_file) {
24622471
}
24632472
}
24642473

2465-
include_paths_stanc3_args <- function(include_paths = NULL, standalone_call = FALSE) {
2474+
#' Build stanc include-path arguments
2475+
#'
2476+
#' Make receives include paths through `STANCFLAGS` and needs paths containing
2477+
#' spaces to be shell-quoted within a single `--include-paths=` flag. Direct
2478+
#' calls through processx instead need the flag and comma-separated paths as
2479+
#' separate, unquoted arguments.
2480+
#'
2481+
#' @param include_paths A character vector of directories containing files used
2482+
#' in Stan `#include` directives, or `NULL`.
2483+
#' @param direct_call A logical indicating whether the arguments will be passed
2484+
#' directly to stanc through processx instead of through Make.
2485+
#'
2486+
#' @return `NULL` if `include_paths` is `NULL`; otherwise, a single
2487+
#' `--include-paths=` argument for Make or two arguments for a direct call.
2488+
#' @noRd
2489+
include_paths_stanc3_args <- function(include_paths = NULL, direct_call = FALSE) {
24662490
stancflags <- NULL
24672491
if (!is.null(include_paths)) {
24682492
assert_dir_exists(include_paths, access = "r")
24692493
include_paths <- sapply(absolute_path(include_paths), wsl_safe_path)
24702494
# Calling stanc3 directly through processx::run does not need quoting
2471-
if (!isTRUE(standalone_call)) {
2495+
if (!isTRUE(direct_call)) {
24722496
paths_w_space <- grep(" ", include_paths)
24732497
include_paths[paths_w_space] <- paste0("'", include_paths[paths_w_space], "'")
24742498
}
24752499
include_paths <- paste0(include_paths, collapse = ",")
24762500
include_paths_flag <- "--include-paths="
2477-
if (isTRUE(standalone_call)) {
2501+
if (isTRUE(direct_call)) {
24782502
stancflags <- c(stancflags, "--include-paths", include_paths)
24792503
} else {
24802504
stancflags <- paste0(stancflags, include_paths_flag, include_paths)
@@ -2494,7 +2518,10 @@ model_variables <- function(stan_file, include_paths = NULL, allow_undefined = F
24942518
command = stanc_cmd(),
24952519
args = c(wsl_safe_path(stan_file),
24962520
"--info",
2497-
include_paths_stanc3_args(include_paths),
2521+
include_paths_stanc3_args(
2522+
include_paths,
2523+
direct_call = TRUE
2524+
),
24982525
allow_undefined_arg),
24992526
wd = cmdstan_path(),
25002527
echo = FALSE,

R/utils.R

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,17 @@ strip_ext <- function(file) {
194194
}
195195
absolute_path <- Vectorize(.absolute_path, USE.NAMES = FALSE)
196196

197+
# Resolve paths when they are stored on a model object rather than when they are
198+
# used, so that later use doesn't depend on the working directory. Empty input
199+
# becomes NULL, the value callers treat as "not set" (absolute_path() would
200+
# otherwise return list()).
201+
resolve_path <- function(path) {
202+
if (!length(path)) {
203+
return(NULL)
204+
}
205+
repair_path(absolute_path(path))
206+
}
207+
197208
# read, write, and copy files --------------------------------------------
198209

199210
#' Copy temporary files (e.g., output, data) to a different location

man/model-method-compile.Rd

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/model-method-model-info.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/helper-models.R

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,24 @@ testing_stan_file <- function(name) {
99
test_path("resources", "stan", paste0(name, ".stan"))
1010
}
1111

12+
local_include_model_with_spaces <- function(.local_envir = parent.frame()) {
13+
model_dir <- withr::local_tempdir(
14+
pattern = "include path",
15+
.local_envir = .local_envir
16+
)
17+
source_files <- c(
18+
testing_stan_file("bernoulli_include"),
19+
testing_stan_file("divide_real_by_two")
20+
)
21+
if (!all(file.copy(source_files, model_dir))) {
22+
stop("Failed to copy Stan include test fixtures.", call. = FALSE)
23+
}
24+
list(
25+
stan_file = file.path(model_dir, "bernoulli_include.stan"),
26+
include_paths = model_dir
27+
)
28+
}
29+
1230
cmdstan_example_file <- function() {
1331
# stan program in different directory from the others
1432
file.path(cmdstan_path(), "examples", "bernoulli", "bernoulli.stan")

tests/testthat/test-fit-shared.R

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -480,19 +480,18 @@ test_that("draws are returned for model with spaces", {
480480
expect_equal(dim(fit$draws()), c(1000, 1, 1))
481481
})
482482

483-
test_that("sampling with inits works with include_paths", {
484-
stan_program_w_include <- testing_stan_file("bernoulli_include")
485-
exe <- cmdstan_ext(strip_ext(stan_program_w_include))
486-
if (file.exists(exe)) {
487-
file.remove(exe)
488-
}
483+
test_that("sampling works with explicit and inferred include paths containing spaces", {
484+
include_model <- local_include_model_with_spaces()
489485

490-
mod_w_include <- cmdstan_model(stan_file = stan_program_w_include,
491-
include_paths = test_path("resources", "stan"))
486+
mod_inferred <- cmdstan_model(stan_file = include_model$stan_file)
487+
expect_equal(
488+
repair_path(mod_inferred$include_paths()),
489+
repair_path(include_model$include_paths)
490+
)
492491

493492
data_list <- list(N = 10, y = c(0,1,0,0,0,0,0,0,0,1))
494493
expect_no_error(utils::capture.output(
495-
fit <- mod_w_include$sample(
494+
fit <- mod_inferred$sample(
496495
data = data_list,
497496
seed = 123,
498497
chains = 4,
@@ -504,6 +503,26 @@ test_that("sampling with inits works with include_paths", {
504503
list(theta = 0.25))
505504
)
506505
))
506+
507+
mod_explicit <- cmdstan_model(
508+
stan_file = include_model$stan_file,
509+
exe_file = mod_inferred$exe_file(),
510+
include_paths = include_model$include_paths,
511+
compile = FALSE
512+
)
513+
expect_equal(
514+
repair_path(mod_explicit$include_paths()),
515+
repair_path(include_model$include_paths)
516+
)
517+
expect_no_error(utils::capture.output(
518+
mod_explicit$sample(
519+
data = data_list,
520+
seed = 123,
521+
chains = 1,
522+
refresh = 500,
523+
init = list(list(theta = 0.25))
524+
)
525+
))
507526
})
508527

509528
test_that("CmdStanModel created with exe_file works", {

0 commit comments

Comments
 (0)