From 83f02963a3e2329ff0cf04068eb08b4183465076 Mon Sep 17 00:00:00 2001 From: jgabry Date: Wed, 16 Sep 2026 19:46:21 -0600 Subject: [PATCH 1/3] Remove what a build and a syntax check leave in the temp directory A build copied the Stan program into the session's temporary directory and left the copy there, with the C++ make generated from it and the executable it linked, since install_executable() copies the executable rather than moving it. check_syntax_stan_file() left the C++ stanc wrote and stanc_info() the JSON it read. All of it now goes when the call ends. From Codex's closing note on the Stage 4 review. Part of #1258. --- NEWS.md | 4 ++++ R/build.R | 3 +++ R/model.R | 7 ++++--- tests/testthat/test-model-rebuild.R | 7 +++++++ tests/testthat/test-stan-file-functions.R | 7 +++++++ 5 files changed, 25 insertions(+), 3 deletions(-) diff --git a/NEWS.md b/NEWS.md index 4af825711..7d7b913b3 100644 --- a/NEWS.md +++ b/NEWS.md @@ -87,6 +87,10 @@ model. (#1258) loaded with `readRDS()`. Previously it reported them as already compiled and calling one failed with a null symbol address, the same defect `$init_model_methods()` had before #1157. +* A build no longer leaves its copy of the Stan program, the C++ generated from +it and a second copy of the executable in the session's temporary directory. +Checking syntax and reading a program's variables no longer leave stanc's +output there either. (#1258) * `$log_prob()`, `$grad_log_prob()`, and other model methods are now faster after initialization because they avoid repeated stale-binding checks. (#1274) * `install_cmdstan()` now offers to copy the `make/local` flags of the diff --git a/R/build.R b/R/build.R index 9e380a8ce..97e36ae20 100644 --- a/R/build.R +++ b/R/build.R @@ -122,6 +122,9 @@ build_executable <- function(stan_file, pattern = "model-", fileext = paste0(".", tools::file_ext(stan_file)) ) file.copy(stan_file, source, overwrite = TRUE) + withr::defer(unlink(c( + source, paste0(strip_ext(source), ".hpp"), cmdstan_ext(strip_ext(source)) + ))) } hpp_code <- get_standalone_hpp( source, c(stanc_inc_paths, stancflags_direct), diff --git a/R/model.R b/R/model.R index ec26350e7..c6cc8694f 100644 --- a/R/model.R +++ b/R/model.R @@ -773,9 +773,9 @@ check_syntax_stan_file <- function(stan_file, stan_file <- resolve_path(stan_file) stanc_options <- assert_valid_stanc_options(stanc_options) stanc_options[["allow-undefined"]] <- TRUE - stanc_options[["o"]] <- wsl_safe_path( - tempfile(pattern = "model-", fileext = ".hpp") - ) + hpp_file <- tempfile(pattern = "model-", fileext = ".hpp") + withr::defer(unlink(hpp_file)) + stanc_options[["o"]] <- wsl_safe_path(hpp_file) if (pedantic) { stanc_options[["warn-pedantic"]] <- TRUE } @@ -2460,6 +2460,7 @@ run_stanc <- function(stan_file, args, spinner = FALSE) { #' @noRd stanc_info <- function(stan_file, include_paths = NULL) { out_file <- tempfile(fileext = ".json") + withr::defer(unlink(out_file)) run_log <- wsl_compatible_run( command = stanc_cmd(), args = c(wsl_safe_path(stan_file), diff --git a/tests/testthat/test-model-rebuild.R b/tests/testthat/test-model-rebuild.R index cf26cf429..ca35cad5e 100644 --- a/tests/testthat/test-model-rebuild.R +++ b/tests/testthat/test-model-rebuild.R @@ -465,3 +465,10 @@ test_that("a model whose Stan file is gone errors, and its executable can be ado adopted <- cmdstan_model(exe_file = mod$exe_file()) expect_sample_output(adopted$sample(data = data, chains = 1), 1) }) + +test_that("a build leaves only the model's C++ in the temporary directory", { + stan_file <- local_program() + before <- list.files(tempdir()) + mod <- cmdstan_model(stan_file) + expect_equal(setdiff(list.files(tempdir()), before), basename(mod$hpp_file())) +}) diff --git a/tests/testthat/test-stan-file-functions.R b/tests/testthat/test-stan-file-functions.R index 316d157d5..cd967b9e8 100644 --- a/tests/testthat/test-stan-file-functions.R +++ b/tests/testthat/test-stan-file-functions.R @@ -23,6 +23,13 @@ test_that("check_syntax_stan_file() checks a program", { expect_true(check_syntax_stan_file(include_model$stan_file, quiet = TRUE)) }) +test_that("check_syntax_stan_file() and variables_stan_file() leave nothing behind", { + before <- list.files(tempdir()) + check_syntax_stan_file(testing_stan_file("bernoulli"), quiet = TRUE) + variables_stan_file(testing_stan_file("bernoulli")) + expect_equal(list.files(tempdir()), before) +}) + test_that("format_stan_file() formats a program", { stan_file <- withr::local_tempfile( lines = "parameters {real y;} model {y ~ std_normal();}", fileext = ".stan" From ef1944689c7d10773e4179eab1094d4bee5f59a3 Mon Sep 17 00:00:00 2001 From: jgabry Date: Thu, 17 Sep 2026 09:32:45 -0600 Subject: [PATCH 2/3] Check the moved-project rule without racing the clock The test that moves a built project to a new directory compared the executable's modification time with a Sys.time() taken microseconds after the copy, and on two Windows runners the file came out newer by under a microsecond. Whether the executable is reused is a decision about the record, so the test now lives in test-model-rebuild-rules.R beside the renamed-program test, in that file's mocked shape: build once, rename the directory, and check that make is not called again. Part of #1258. --- tests/testthat/test-model-rebuild-rules.R | 14 ++++++++++++++ tests/testthat/test-model-rebuild.R | 14 -------------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/testthat/test-model-rebuild-rules.R b/tests/testthat/test-model-rebuild-rules.R index c6b17fd68..b656f0383 100644 --- a/tests/testthat/test-model-rebuild-rules.R +++ b/tests/testthat/test-model-rebuild-rules.R @@ -199,6 +199,20 @@ test_that("a renamed program with a copied executable rebuilds for its name", { ))) }) +test_that("a project moved as a whole still reuses its executable", { + stan_file <- local_bernoulli() + mocked(expect_mock_compile(cmdstan_model(stan_file))) + + moved <- paste0(dirname(stan_file), "-moved") + withr::defer(unlink(moved, recursive = TRUE)) + file.rename(dirname(stan_file), moved) + + mocked(expect_no_mock_compile(expect_interactive_message( + cmdstan_model(file.path(moved, basename(stan_file))), + "Model executable is up to date!" + ))) +}) + test_that("a record that cannot be used rebuilds and says why", { stan_file <- local_bernoulli() mocked(expect_mock_compile(mod <- cmdstan_model(stan_file))) diff --git a/tests/testthat/test-model-rebuild.R b/tests/testthat/test-model-rebuild.R index ca35cad5e..0e11192a4 100644 --- a/tests/testthat/test-model-rebuild.R +++ b/tests/testthat/test-model-rebuild.R @@ -270,20 +270,6 @@ test_that("include paths reordered to the same resolution do not rebuild", { ) }) -test_that("a project moved as a whole still reuses its executable", { - dir_a <- withr::local_tempdir() - stan_file_a <- seed_from( - canonical_bernoulli_stan, canonical_bernoulli_exe, dir_a - ) - dir_b <- paste0(dir_a, "-moved") - withr::defer(unlink(dir_b, recursive = TRUE)) - file.rename(dir_a, dir_b) - - expect_no_recompilation( - cmdstan_model(file.path(dir_b, basename(stan_file_a))) - ) -}) - test_that("formatting the program invalidates the executable that built it", { dir <- withr::local_tempdir() stan_file <- write_stan_file(c( From 6bf0be344e08f607589b040664d5889d83de609f Mon Sep 17 00:00:00 2001 From: jgabry Date: Thu, 17 Sep 2026 15:31:04 -0600 Subject: [PATCH 3/3] Give the WSL check four hours before GitHub kills it A WSL run takes about two hours and twenty minutes. Twice on 2026-09-17 a run stalled inside R CMD check with nothing printed, once until the runner lost contact after three hours and once until it was cancelled by hand after five. Without a job timeout a stalled run holds a runner for the six-hour default and prints no testthat output when it dies. --- .github/workflows/R-CMD-check-wsl.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/R-CMD-check-wsl.yaml b/.github/workflows/R-CMD-check-wsl.yaml index c06cf17df..a2a6200a9 100644 --- a/.github/workflows/R-CMD-check-wsl.yaml +++ b/.github/workflows/R-CMD-check-wsl.yaml @@ -21,6 +21,7 @@ jobs: WSL-R-CMD-check: if: "! contains(github.event.head_commit.message, '[ci skip]')" runs-on: windows-latest + timeout-minutes: 240 name: windows-latest-WSL2