Skip to content

Commit 8b92b81

Browse files
committed
Make get_cmdstan_flags() robust to noisy make output
Fixes #1162 Parse `make print-<FLAG>` output line-by-line so cmdstanr ignores unrelated `make` chatter such as directory-entering messages triggered by `MAKEFLAGS=-w`. This fixes a failure where `get_cmdstan_flags("STANCFLAGS")` could return junk tokens, which then caused `cmdstan_model()` to error when checking local stanc flags. Also: - return `character(0)` for empty `STANCFLAGS` - use a length-safe check when combining local stanc flags - add regression tests for noisy make output, realistic continued `STANCFLAGS`, and the reported `MAKEFLAGS="-w -j 4"` case
1 parent 418b1c5 commit 8b92b81

6 files changed

Lines changed: 178 additions & 31 deletions

File tree

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
keywords (@VisruthSK, #1154)
55
* `save_cmdstan_config` and `save_metric` default to `FALSE` but can be
66
set to `TRUE` for an entire R session via new global options. (#1159)
7+
* `cmdstan_model()` no longer fails when `MAKEFLAGS` enables directory-printing
8+
output while reading `STANCFLAGS` from `make`. (#1163)
79

810
* CmdStanModel objects created using `compile_model_methods = TRUE` that are
911
then saved and reloaded no longer error in model fitting methods. Model methods

R/cpp_opts.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ assert_valid_threads <- function(threads, cpp_options, multiple_chains = FALSE)
174174
# exe_info style means off is FALSE
175175

176176
exe_info_style_cpp_options <- function(cpp_options) {
177-
if(is.null(cpp_options)) cpp_options <- list()
177+
if (is.null(cpp_options)) cpp_options <- list()
178178
names(cpp_options) <- tolower(names(cpp_options))
179179
flags_reported_in_exe_info <- c(
180180
"stan_threads", "stan_mpi", "stan_opencl",

R/model.R

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ compile <- function(quiet = TRUE,
628628
}
629629
stancflags_combined <- stanc_built_options
630630
stancflags_local <- get_cmdstan_flags("STANCFLAGS")
631-
if (stancflags_local != "") {
631+
if (length(stancflags_local) > 0) {
632632
stancflags_combined <- c(stancflags_combined, stancflags_local)
633633
}
634634
stanc_inc_paths <- include_paths_stanc3_args(include_paths, standalone_call = TRUE)
@@ -730,7 +730,7 @@ compile <- function(quiet = TRUE,
730730
private$precompile_stanc_options_ <- NULL
731731
private$precompile_include_paths_ <- NULL
732732

733-
if(!dry_run) {
733+
if (!dry_run) {
734734
if (compile_model_methods) {
735735
expose_model_methods(env = private$model_methods_env_, verbose = !quiet)
736736
}

R/utils.R

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -561,7 +561,7 @@ wsl_installed <- function() {
561561
p <- processx::process$new("wsl", "uname")
562562
for(i in 1:50) {
563563
Sys.sleep(0.1)
564-
if(!p$is_alive()) {
564+
if (!p$is_alive()) {
565565
break
566566
}
567567
}
@@ -686,32 +686,55 @@ assert_dir_exists <- checkmate::makeAssertionFunction(check_dir_exists)
686686
assert_file_exists <- checkmate::makeAssertionFunction(check_file_exists)
687687

688688
# Model methods & expose_functions helpers ------------------------------------------------------
689+
690+
# Extract the requested make variable from `make print-<FLAG>` output while
691+
# ignoring unrelated lines
692+
parse_make_print_flag <- function(flag_name, stdout) {
693+
lines <- strsplit(stdout, "\r?\n", perl = TRUE)[[1]]
694+
pattern <- paste0("^\\s*", flag_name, "\\s*(=|\\+=)\\s*")
695+
matches <- grep(pattern, lines, perl = TRUE)
696+
697+
if (length(matches) == 0) {
698+
stop(
699+
"Failed to parse `", flag_name, "` from `make print-", flag_name, "` output.\n",
700+
"Output was:\n", stdout,
701+
call. = FALSE
702+
)
703+
}
704+
if (length(matches) > 1) {
705+
stop(
706+
"Found multiple `", flag_name, "` lines in `make print-", flag_name, "` output.\n",
707+
"Output was:\n", stdout,
708+
call. = FALSE
709+
)
710+
}
711+
712+
sub(pattern, "", trimws(lines[matches]), perl = TRUE)
713+
}
714+
689715
get_cmdstan_flags <- function(flag_name) {
690716
cmdstan_path <- cmdstanr::cmdstan_path()
691717
withr::with_envvar(
692718
c("HOME" = short_path(Sys.getenv("HOME"))),
693-
flags <- wsl_compatible_run(
719+
flags_stdout <- wsl_compatible_run(
694720
command = "make",
695721
args = c("-s", paste0("print-", flag_name)),
696722
wd = cmdstan_path
697723
)$stdout
698724
)
699-
700-
flags <- gsub("\n", "", flags, fixed = TRUE)
701-
702-
flags <- gsub(
703-
pattern = paste0(flag_name, "\\s(=|\\+=)(\\s|$)"),
704-
replacement = "", x = flags
705-
)
706-
707-
if (flags == "") {
708-
return(flags)
709-
}
725+
flags <- parse_make_print_flag(flag_name, flags_stdout)
710726

711727
if (flag_name == "STANCFLAGS") {
712728
# StanC flags need to be returned as a character vector
713-
flags_vec <- strsplit(x = flags, split = " ", fixed = TRUE)[[1]]
714-
return(flags_vec)
729+
if (!nzchar(flags)) {
730+
return(character())
731+
}
732+
flags_vec <- strsplit(x = trimws(flags), split = "\\s+", perl = TRUE)[[1]]
733+
return(flags_vec[nzchar(flags_vec)])
734+
}
735+
736+
if (!nzchar(flags)) {
737+
return(flags)
715738
}
716739

717740
if (flag_name %in% c("LDLIBS", "LDFLAGS_TBB")) {
@@ -753,9 +776,11 @@ check_sundials_fpic <- function(verbose) {
753776
return(invisible(NULL))
754777
}
755778
if (interactive()) {
756-
message("SUNDIALS needs to be compiled with -fPIC when exposing functions or ",
757-
"model methods on Linux.\n",
758-
"Updating your make/local file to include -fPIC and rebuilding CmdStan now...")
779+
message(
780+
"SUNDIALS needs to be compiled with -fPIC when exposing functions or ",
781+
"model methods on Linux.\n",
782+
"Updating your make/local file to include -fPIC and rebuilding CmdStan now..."
783+
)
759784
}
760785
cmdstan_make_local(cpp_options = list("CPPFLAGS_SUNDIALS += -fPIC"), append = TRUE)
761786
rebuild_cmdstan(quiet = !verbose)

tests/testthat/test-model-compile.R

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -831,15 +831,27 @@ test_that("dirname of stan_file is used as include path if no other paths suppli
831831
expect_s3_class(mod_tmp$compile(), "CmdStanModel")
832832
})
833833

834-
test_that("STANCFLAGS included from make/local", {
835-
make_local_old <- cmdstan_make_local()
836-
cmdstan_make_local(cpp_options = "STANCFLAGS += --O1", append = TRUE)
837-
out <- utils::capture.output(mod$compile(quiet = FALSE, force_recompile = TRUE))
834+
test_that("STANCFLAGS from get_cmdstan_flags() are included in compile output", {
835+
real_get_cmdstan_flags <- get_cmdstan_flags
836+
out <- with_mocked_bindings(
837+
utils::capture.output(mod$compile(quiet = FALSE, force_recompile = TRUE)),
838+
get_cmdstan_flags = function(flag_name) {
839+
if (identical(flag_name, "STANCFLAGS")) {
840+
c("--O1", "--warn-pedantic")
841+
} else {
842+
real_get_cmdstan_flags(flag_name)
843+
}
844+
}
845+
)
838846
if(os_is_windows() && !os_is_wsl()) {
839-
out_w_flags <- "bin/stanc.exe --name='bernoulli_model' --O1 --o"
847+
out_w_flags <- "bin/stanc.exe --name='bernoulli_model'[[:space:]]+--O1[[:space:]]+--warn-pedantic[[:space:]]+--o"
840848
} else {
841-
out_w_flags <- "bin/stanc --name='bernoulli_model' --O1 --o"
849+
out_w_flags <- "bin/stanc --name='bernoulli_model'[[:space:]]+--O1[[:space:]]+--warn-pedantic[[:space:]]+--o"
842850
}
843851
expect_output(print(out), out_w_flags)
844-
cmdstan_make_local(cpp_options = make_local_old, append = FALSE)
852+
})
853+
854+
test_that("compile() ignores directory chatter from MAKEFLAGS when reading STANCFLAGS", {
855+
withr::local_envvar(MAKEFLAGS = "-w -j 4")
856+
expect_compilation(mod, quiet = TRUE, force_recompile = TRUE)
845857
})

tests/testthat/test-utils.R

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,115 @@ test_that("get_cmdstan_flags() can be used recursively in `make`", {
277277
)
278278
return(invisible())
279279
}
280-
stdo <- recursive_run$stdout
281-
recursive_flags <- readLines(textConnection(stdo))
282-
expect_equal(nonrecursive_flags, recursive_flags)
280+
expected_stdout <- paste(capture.output(cat(nonrecursive_flags)), collapse = "\n")
281+
expect_equal(recursive_run$stdout, expected_stdout)
282+
})
283+
284+
test_that("parse_make_print_flag() ignores unrelated make output", {
285+
stdout <- paste(
286+
"make: Entering directory '/tmp/cmdstan'",
287+
"STANCFLAGS = --O1 --warn-pedantic --allow-undefined",
288+
"make: Leaving directory '/tmp/cmdstan'",
289+
sep = "\n"
290+
)
291+
292+
expect_equal(
293+
parse_make_print_flag("STANCFLAGS", stdout),
294+
"--O1 --warn-pedantic --allow-undefined"
295+
)
296+
})
297+
298+
test_that("parse_make_print_flag() errors if no matching flag line is found", {
299+
expect_error(
300+
parse_make_print_flag("STANCFLAGS", "make: Entering directory '/tmp/cmdstan'"),
301+
"Failed to parse `STANCFLAGS`",
302+
fixed = TRUE
303+
)
304+
})
305+
306+
test_that("parse_make_print_flag() errors if multiple matching flag lines are found", {
307+
stdout <- paste(
308+
"STANCFLAGS = --O1",
309+
"STANCFLAGS = --warn-pedantic",
310+
sep = "\n"
311+
)
312+
expect_error(
313+
parse_make_print_flag("STANCFLAGS", stdout),
314+
"Found multiple `STANCFLAGS` lines",
315+
fixed = TRUE
316+
)
317+
})
318+
319+
test_that("get_cmdstan_flags() returns empty STANCFLAGS as character(0)", {
320+
with_mocked_bindings(
321+
{
322+
expect_equal(get_cmdstan_flags("STANCFLAGS"), character(0))
323+
},
324+
wsl_compatible_run = function(...) {
325+
list(stdout = "STANCFLAGS =\n")
326+
}
327+
)
328+
})
329+
330+
test_that("get_cmdstan_flags() preserves empty non-STANCFLAGS values", {
331+
with_mocked_bindings(
332+
{
333+
expect_equal(get_cmdstan_flags("CPPFLAGS"), "")
334+
},
335+
wsl_compatible_run = function(...) {
336+
list(stdout = "CPPFLAGS =\n")
337+
}
338+
)
339+
})
340+
341+
test_that("get_cmdstan_flags() handles line-continuation STANCFLAGS in make/local", {
342+
tmpdir <- withr::local_tempdir()
343+
# Build a minimal make setup so we can exercise real make line continuations.
344+
writeLines(
345+
c(
346+
"print-%: ; @echo $* = $($*)",
347+
"-include local"
348+
),
349+
file.path(tmpdir, "Makefile")
350+
)
351+
writeLines(
352+
c(
353+
"STANCFLAGS += --O1 \\",
354+
" --warn-pedantic \\",
355+
" --allow-undefined"
356+
),
357+
file.path(tmpdir, "local")
358+
)
359+
make_run <- processx::run(
360+
command = "make",
361+
args = c("-s", "print-STANCFLAGS"),
362+
wd = tmpdir,
363+
error_on_status = FALSE
364+
)
365+
if (make_run$status != 0) {
366+
fail(
367+
paste(
368+
"Mini make failed.",
369+
paste0("status: ", make_run$status),
370+
"stdout:",
371+
make_run$stdout,
372+
"stderr:",
373+
make_run$stderr,
374+
sep = "\n"
375+
)
376+
)
377+
return(invisible())
378+
}
379+
380+
with_mocked_bindings(
381+
{
382+
expect_equal(
383+
get_cmdstan_flags("STANCFLAGS"),
384+
c("--O1", "--warn-pedantic", "--allow-undefined")
385+
)
386+
},
387+
wsl_compatible_run = function(...) {
388+
list(stdout = make_run$stdout)
389+
}
390+
)
283391
})

0 commit comments

Comments
 (0)