diff --git a/R/build_record.R b/R/build_record.R index ddf75f9f6..22bfa253a 100644 --- a/R/build_record.R +++ b/R/build_record.R @@ -659,8 +659,8 @@ assess_build <- function(expected, current) { #' `"unavailable"`, and `reason`, which is `NULL` when the record is available #' and otherwise one of `"missing"` (no record beside the executable), #' `"unreadable"` (a record that could not be read), `"executable_mismatch"` -#' (the record describes a different executable, so the one at this path was -#' replaced after the record was written) or `"unsupported_format"` (written by +#' (the record describes a different executable, so the one at this path has +#' changed since the record was written) or `"unsupported_format"` (written by #' a CmdStanR that stores records differently, in which case the result also #' has a `format_version` field). #' @@ -681,9 +681,12 @@ assess_build <- function(expected, current) { #' * `stanc_options`: the flags as given to stanc, in order. #' For example, `list(O1 = TRUE)` and `list("O1")` both come back as #' `list("--O1")`. +#' * `stanc_options_from_make`: the flags `make/local` added to the stanc +#' call through `STANCFLAGS`. A flag that `stanc_options` also sets is not +#' repeated here since `stanc_options` takes precedence. #' * `include_paths`: a character vector of the directories searched for -#' included files. When none were given, this is the Stan program's own -#' directory if the program has includes and empty otherwise. +#' included files. When none were provided but the Stan program has includes +#' this is set to the program's own directory. #' #' * `dependencies`: A list describing the files the build read. Contains sublists #' `stan_file`, `included_files`, `user_header` and `make_local`. `user_header` @@ -710,10 +713,8 @@ assess_build <- function(expected, current) { #' was found in. #' #' The result leaves out some of what the record holds: the file hashes the -#' rebuild check compares, the stanc flags CmdStanR added or `make/local` -#' contributed, the model name given to stanc, and the TBB directory. -#' `dependencies$make_local` names the file those flags came from, though its -#' contents may have changed since the build. +#' rebuild check compares, the stanc flags CmdStanR added itself, the model +#' name given to stanc, and the TBB directory. #' #' Absent items and empty items have different interpretations. A field missing #' from the result means there was no usable record to read it from. An empty @@ -746,6 +747,7 @@ stan_build_info <- function(exe_file) { info$configuration <- list( cpp_options = record$configuration$cpp_options, stanc_options = record$configuration$stanc_options, + stanc_options_from_make = record$configuration$stanc_options_from_make, include_paths = as.character(unlist(record$configuration$include_paths)) ) info$dependencies <- public_dependencies(record$dependencies) @@ -847,8 +849,12 @@ print.stan_build_info <- function(x, ...) { stanc_options <- unlist(x$configuration$stanc_options) cat(" stanc_options: ", if (length(stanc_options) == 0) "none" else paste(stanc_options, collapse = " "), "\n", sep = "") - cat(" include_paths: ", paste(x$configuration$include_paths, collapse = ", "), - "\n", sep = "") + from_make <- unlist(x$configuration$stanc_options_from_make) + cat(" stanc_options_from_make: ", if (length(from_make) == 0) "none" else + paste(from_make, collapse = " "), "\n", sep = "") + include_paths <- x$configuration$include_paths + cat(" include_paths: ", if (length(include_paths) == 0) "none" else + paste(include_paths, collapse = ", "), "\n", sep = "") cat("Dependencies:\n") path_line <- function(label, entry) { @@ -890,24 +896,24 @@ build_record_status_line <- function(x) { } switch(x$record$reason, missing = paste0( - "Build record: none. There is no build record beside this executable, ", - "so only what it reports about itself is known." + "Build record: not found. Only what the executable says about itself ", + "is known." ), - unreadable = "Build record: could not be read.", - executable_mismatch = paste0( - "Build record: does not match this executable, which was replaced ", - "after the record was written." + unreadable = paste0( + "Build record: could not be read. Rebuilding the executable writes a ", + "new one." ), + executable_mismatch = + "Build record: does not match. Executable changed after the build.", unsupported_format = if (x$format_version > build_record_format_version) { paste0( - "Build record: written in format ", x$format_version, " by a newer ", - "version of CmdStanR. Upgrade CmdStanR to read it." + "Build record: format ", x$format_version, " (newer CmdStanR). ", + "Upgrade CmdStanR to read it." ) } else { paste0( - "Build record: written in format ", x$format_version, " by an older ", - "version of CmdStanR. To get a record this version reads, rebuild ", - "the executable." + "Build record: format ", x$format_version, " (older CmdStanR). ", + "Rebuild the executable to replace it." ) } ) diff --git a/dev-notes/compilation-state-contract.md b/dev-notes/compilation-state-contract.md index 0222caf85..fd60e5941 100644 --- a/dev-notes/compilation-state-contract.md +++ b/dev-notes/compilation-state-contract.md @@ -980,6 +980,7 @@ list( configuration = list( cpp_options = list(STAN_THREADS = "true"), stanc_options = list(), + stanc_options_from_make = list("--O1"), include_paths = "/proj" ), dependencies = list( diff --git a/dev-notes/compilation-state.md b/dev-notes/compilation-state.md index 4297eb62e..1c0628375 100644 --- a/dev-notes/compilation-state.md +++ b/dev-notes/compilation-state.md @@ -2994,11 +2994,12 @@ shipped yet, so the set can still be chosen freely; after 1.0 it cannot. The has fail that test. The artifact hash answers a question the caller can already answer by hashing the file whose path they just passed in, and the dependency hashes compare against nothing but another record's same field. So do -`stanc_options_added`, `stanc_options_from_make` and `stanc_name`, which say how -cmdstanr assembled the stanc command line rather than what was asked of it. `tbb_dir` is out on the same +`stanc_options_added` and `stanc_name`, which say how cmdstanr assembled the stanc +command line rather than what was asked of it. `tbb_dir` is out on the same test; the record keeps it because Windows needs it at launch (ยง4). `configuration` keeps -`include_paths` for diagnosis alone: a caller debugging an include has no other way -to see where the build searched. +`include_paths` and `stanc_options_from_make` for diagnosis alone: a caller debugging +an include has no other way to see where the build searched, and `make/local` can +change after a build, so the file alone does not say which flags the build saw. @@ -3017,6 +3018,7 @@ list( configuration = list( cpp_options = list(STAN_THREADS = "true"), stanc_options = list(), + stanc_options_from_make = list("--O1"), include_paths = "/proj" ), dependencies = list( diff --git a/man/stan_build_info.Rd b/man/stan_build_info.Rd index fd346fc36..050f6ea02 100644 --- a/man/stan_build_info.Rd +++ b/man/stan_build_info.Rd @@ -23,8 +23,8 @@ A list of class \code{"stan_build_info"}. Two fields are always there: \code{"unavailable"}, and \code{reason}, which is \code{NULL} when the record is available and otherwise one of \code{"missing"} (no record beside the executable), \code{"unreadable"} (a record that could not be read), \code{"executable_mismatch"} -(the record describes a different executable, so the one at this path was -replaced after the record was written) or \code{"unsupported_format"} (written by +(the record describes a different executable, so the one at this path has +changed since the record was written) or \code{"unsupported_format"} (written by a CmdStanR that stores records differently, in which case the result also has a \code{format_version} field). \item \code{reported_features}: A list containing what the executable reports about @@ -46,9 +46,12 @@ comes back as \code{list(STAN_THREADS = "true")}. \item \code{stanc_options}: the flags as given to stanc, in order. For example, \code{list(O1 = TRUE)} and \code{list("O1")} both come back as \code{list("--O1")}. +\item \code{stanc_options_from_make}: the flags \code{make/local} added to the stanc +call through \code{STANCFLAGS}. A flag that \code{stanc_options} also sets is not +repeated here since \code{stanc_options} takes precedence. \item \code{include_paths}: a character vector of the directories searched for -included files. When none were given, this is the Stan program's own -directory if the program has includes and empty otherwise. +included files. When none were provided but the Stan program has includes +this is set to the program's own directory. } \item \code{dependencies}: A list describing the files the build read. Contains sublists \code{stan_file}, \code{included_files}, \code{user_header} and \code{make_local}. \code{user_header} @@ -74,10 +77,8 @@ was found in. } The result leaves out some of what the record holds: the file hashes the -rebuild check compares, the stanc flags CmdStanR added or \code{make/local} -contributed, the model name given to stanc, and the TBB directory. -\code{dependencies$make_local} names the file those flags came from, though its -contents may have changed since the build. +rebuild check compares, the stanc flags CmdStanR added itself, the model +name given to stanc, and the TBB directory. Absent items and empty items have different interpretations. A field missing from the result means there was no usable record to read it from. An empty diff --git a/tests/testthat/_snaps/build-info.md b/tests/testthat/_snaps/build-info.md new file mode 100644 index 000000000..d3564e3ea --- /dev/null +++ b/tests/testthat/_snaps/build-info.md @@ -0,0 +1,114 @@ +# print.stan_build_info() output for each record state + + Code + print(full_x) + Output + Build record: available + Reported features: + stan_threads: TRUE + stan_mpi: FALSE + stan_opencl: FALSE + stan_no_range_checks: unknown + stan_version: 2.39.0 + Configuration: + cpp_options: STAN_THREADS=true STAN_CPP_OPTIMS=true + stanc_options: --O1 + stanc_options_from_make: --warn-pedantic + include_paths: /proj/inc, /proj/shared + Dependencies: + stan_file: /proj/bernoulli.stan + included_file: /proj/inc/half.stan + included_file: /proj/shared/prior.stan (no longer exists) + user_header: /proj/helpers.hpp + make_local: /opt/cmdstan-2.39.0/make/local + CmdStan 2.39.0 at /opt/cmdstan-2.39.0 + Dependencies CmdStanR does not track: + make/local includes another makefile (/opt/cmdstan-2.39.0/make/local) + the user header includes other headers (/proj/helpers.hpp) + +--- + + Code + print(sparse_x) + Output + Build record: available + Reported features: + stan_threads: TRUE + stan_mpi: unknown + stan_opencl: FALSE + stan_no_range_checks: unknown + stan_version: 2.39.0 + Configuration: + cpp_options: STAN_THREADS=true + stanc_options: --O1 + stanc_options_from_make: none + include_paths: none + Dependencies: + stan_file: bernoulli.stan (no longer exists) + CmdStan 2.39.0 at /opt/cmdstan-2.39.0 (no longer exists) + +--- + + Code + print(missing_x) + Output + Build record: not found. Only what the executable says about itself is known. + Reported features: + stan_threads: unknown + stan_mpi: unknown + stan_opencl: unknown + stan_no_range_checks: unknown + stan_version: unknown + +--- + + Code + print(unreadable_x) + Output + Build record: could not be read. Rebuilding the executable writes a new one. + Reported features: + stan_threads: unknown + stan_mpi: unknown + stan_opencl: unknown + stan_no_range_checks: unknown + stan_version: unknown + +--- + + Code + print(mismatch_x) + Output + Build record: does not match. Executable changed after the build. + Reported features: + stan_threads: unknown + stan_mpi: unknown + stan_opencl: unknown + stan_no_range_checks: unknown + stan_version: unknown + +--- + + Code + print(newer_x) + Output + Build record: format 2 (newer CmdStanR). Upgrade CmdStanR to read it. + Reported features: + stan_threads: unknown + stan_mpi: unknown + stan_opencl: unknown + stan_no_range_checks: unknown + stan_version: unknown + +--- + + Code + print(older_x) + Output + Build record: format 0 (older CmdStanR). Rebuild the executable to replace it. + Reported features: + stan_threads: unknown + stan_mpi: unknown + stan_opencl: unknown + stan_no_range_checks: unknown + stan_version: unknown + diff --git a/tests/testthat/test-build-info.R b/tests/testthat/test-build-info.R index 74a85ecc6..1cb38bc2a 100644 --- a/tests/testthat/test-build-info.R +++ b/tests/testthat/test-build-info.R @@ -53,10 +53,15 @@ test_that("an available build record is read in full without launching the execu expect_identical(features$stan_version, "2.39.0") expect_named( - result$configuration, c("cpp_options", "stanc_options", "include_paths") + result$configuration, + c( + "cpp_options", "stanc_options", "stanc_options_from_make", + "include_paths" + ) ) expect_equal(result$configuration$cpp_options, list(STAN_THREADS = "true")) expect_equal(result$configuration$stanc_options, list("--O1")) + expect_equal(result$configuration$stanc_options_from_make, list()) expect_type(result$configuration$include_paths, "character") expect_named( @@ -103,10 +108,25 @@ test_that("fields the record withholds are absent from the result", { expect_named(result$dependencies$make_local, c("built_from", "exists")) expect_false("stanc_options_added" %in% names(result$configuration)) - expect_false("stanc_options_from_make" %in% names(result$configuration)) expect_false("stanc_name" %in% names(result$configuration)) expect_named( - result$configuration, c("cpp_options", "stanc_options", "include_paths") + result$configuration, + c( + "cpp_options", "stanc_options", "stanc_options_from_make", + "include_paths" + ) + ) +}) + +test_that("the stanc flags make/local added come back as recorded", { + result <- available_result(function(record) { + record$configuration$stanc_options_from_make <- + list("--O0", "--warn-pedantic") + record + }) + expect_equal( + result$configuration$stanc_options_from_make, + list("--O0", "--warn-pedantic") ) }) @@ -464,13 +484,13 @@ test_that("features reported by the binary have the fixed shape", { expect_identical(features$stan_version, "2.39.0") }) -test_that("print.stan_build_info() shows the fragments the spec pins", { +test_that("print.stan_build_info() output for each record state", { unavailable_features <- list( stan_threads = NA, stan_mpi = NA, stan_opencl = NA, stan_no_range_checks = NA, stan_version = NA_character_ ) - available_x <- structure( + sparse_x <- structure( list( record = list(status = "available", reason = NULL), reported_features = list( @@ -480,6 +500,7 @@ test_that("print.stan_build_info() shows the fragments the spec pins", { configuration = list( cpp_options = list(STAN_THREADS = "true"), stanc_options = list("--O1"), + stanc_options_from_make = list(), include_paths = character(0) ), dependencies = list( @@ -488,10 +509,10 @@ test_that("print.stan_build_info() shows the fragments the spec pins", { user_header = NULL, make_local = NULL ), - cmdstan = list(path = "/opt/cmdstan-2.39.0", version = "2.39.0", exists = FALSE), - untracked_dependencies = list( - list(kind = "make_local_include", detected_in = "make/local") - ) + cmdstan = list( + path = "/opt/cmdstan-2.39.0", version = "2.39.0", exists = FALSE + ), + untracked_dependencies = list() ), class = "stan_build_info" ) @@ -533,17 +554,50 @@ test_that("print.stan_build_info() shows the fragments the spec pins", { class = "stan_build_info" ) - expect_output(print(available_x), "available", fixed = TRUE) - expect_output(print(missing_x), "no build record", fixed = TRUE) - expect_output(print(unreadable_x), "could not be read", fixed = TRUE) - expect_output(print(mismatch_x), "does not match", fixed = TRUE) - expect_output(print(newer_x), "newer version of CmdStanR", fixed = TRUE) - expect_output(print(older_x), "rebuild", fixed = TRUE) - expect_output(print(available_x), "stan_threads: TRUE", fixed = TRUE) - expect_output(print(available_x), "stan_mpi: unknown", fixed = TRUE) - expect_output(print(available_x), "no longer exists", fixed = TRUE) - expect_output( - print(available_x), "make/local includes another makefile", fixed = TRUE + full_x <- structure( + list( + record = list(status = "available", reason = NULL), + reported_features = list( + stan_threads = TRUE, stan_mpi = FALSE, stan_opencl = FALSE, + stan_no_range_checks = NA, stan_version = "2.39.0" + ), + configuration = list( + cpp_options = list(STAN_THREADS = "true", STAN_CPP_OPTIMS = "true"), + stanc_options = list("--O1"), + stanc_options_from_make = list("--warn-pedantic"), + include_paths = c("/proj/inc", "/proj/shared") + ), + dependencies = list( + stan_file = list(built_from = "/proj/bernoulli.stan", exists = TRUE), + included_files = list( + list(built_from = "/proj/inc/half.stan", exists = TRUE), + list(built_from = "/proj/shared/prior.stan", exists = FALSE) + ), + user_header = list(built_from = "/proj/helpers.hpp", exists = TRUE), + make_local = list( + built_from = "/opt/cmdstan-2.39.0/make/local", exists = TRUE + ) + ), + cmdstan = list( + path = "/opt/cmdstan-2.39.0", version = "2.39.0", exists = TRUE + ), + untracked_dependencies = list( + list( + kind = "make_local_include", + detected_in = "/opt/cmdstan-2.39.0/make/local" + ), + list(kind = "user_header_include", detected_in = "/proj/helpers.hpp") + ) + ), + class = "stan_build_info" ) - expect_output(expect_invisible(print(available_x))) + + expect_snapshot(print(full_x)) + expect_snapshot(print(sparse_x)) + expect_snapshot(print(missing_x)) + expect_snapshot(print(unreadable_x)) + expect_snapshot(print(mismatch_x)) + expect_snapshot(print(newer_x)) + expect_snapshot(print(older_x)) + expect_output(expect_invisible(print(sparse_x))) }) diff --git a/vignettes/cmdstanr-internals.Rmd b/vignettes/cmdstanr-internals.Rmd index 741614db9..ef4b7b11e 100644 --- a/vignettes/cmdstanr-internals.Rmd +++ b/vignettes/cmdstanr-internals.Rmd @@ -39,13 +39,15 @@ The `cmdstan_model()` function creates a new `CmdStanModel` object, which stores the path to a Stan program as well as the path to a compiled executable. ```{r compile} -# Copy the example to a tempfile so the vignette doesn't modify -# the CmdStan installation. +# Copy the example to the temporary directory so the vignette doesn't modify +# the CmdStan installation example_stan_file <- file.path( cmdstan_path(), "examples", "bernoulli", "bernoulli.stan" ) -stan_file <- tempfile(pattern = "bernoulli-", fileext = ".stan") +stan_file <- file.path(tempdir(), "bernoulli.stan") invisible(file.copy(example_stan_file, stan_file)) + +# Compile the model mod <- cmdstan_model(stan_file) mod$print() mod$stan_file() @@ -252,7 +254,22 @@ itself. The file is named after the executable, so `bernoulli` is described by `.bernoulli.cmdstanr.json`. ```{r build-record} -list.files(dirname(mod$exe_file()), pattern = "cmdstanr.json", all.files = TRUE) +list.files( + dirname(mod$exe_file()), pattern = "bernoulli.*cmdstanr.json", all.files = TRUE +) +``` + +The `$build_info()` method reads the record back. The standalone +`stan_build_info()` function does the same for a path to an executable when you +don't have a model object. When there is no usable record it says why and +reports only what the executable says about itself. + +```{r build-info} +mod$build_info() + +# demonstrating stan_build_info even though here we have the model object +# so we can use mod$build_info() +stan_build_info(mod$exe_file()) ``` If the directory is under version control, we recommend adding the record file