diff --git a/R/rxp_io.R b/R/rxp_io.R index 36c01de..49bb8aa 100644 --- a/R/rxp_io.R +++ b/R/rxp_io.R @@ -644,12 +644,31 @@ rxp_jl_file <- function(...) rxp_file("Jl", ...) #' @return A list with elements: `name`, `snippet`, `type`, `additional_files`, #' `nix_env`. #' @noRd -rxp_common_setup <- function(out_name, expr_str, nix_env, direction) { +rxp_common_setup <- function( + out_name, + expr_str, + nix_env, + direction, + env_var = NULL +) { expr_str <- gsub("\"", "'", expr_str) base <- sanitize_nix_env(nix_env) + # Always set RETICULATE_AUTOCONFIGURE=0 to prevent reticulate from + # modifying PYTHONPATH in Nix's hermetic build environment + default_env <- c(RETICULATE_AUTOCONFIGURE = "0") + + # Merge with user-provided env_var if present (user values take precedence) + if (!is.null(env_var) && length(env_var) > 0) { + env_var <- c(default_env[!names(default_env) %in% names(env_var)], env_var) + } else { + env_var <- default_env + } + + env_exports <- build_env_exports(env_var) + r_command <- build_transfer_command(out_name, expr_str, direction) - build_phase <- build_reticulate_phase(r_command) + build_phase <- build_reticulate_phase(r_command, env_exports) snippet <- make_derivation_snippet( out_name = out_name, @@ -665,7 +684,8 @@ rxp_common_setup <- function(out_name, expr_str, nix_env, direction) { snippet = snippet, type = paste0("rxp_", direction), additional_files = "", - nix_env = nix_env + nix_env = nix_env, + env_var = env_var # Store for potential inspection/debugging ), class = "rxp_derivation" ) @@ -704,9 +724,10 @@ build_transfer_command <- function(out_name, expr_str, direction) { #' @param r_command Character R command #' @return Character build phase #' @noRd -build_reticulate_phase <- function(r_command) { +build_reticulate_phase <- function(r_command, env_exports = "") { sprintf( - "export RETICULATE_PYTHON=${defaultPkgs.python3}/bin/python\n Rscript -e \"\n source('libraries.R')\n%s\"", + "# Clear any inherited PYTHONPATH\n unset PYTHONPATH || true\n # Dynamically determine Python version and set PYTHONPATH\n PYTHON_VERSION=$(${defaultPkgs.python3}/bin/python3 -c 'import sys; print(f\"{sys.version_info.major}.{sys.version_info.minor}\")')\n export PYTHONPATH=${defaultPkgs.python3}/lib/python$PYTHON_VERSION/site-packages\n export RETICULATE_PYTHON=${defaultPkgs.python3}/bin/python\n export RETICULATE_AUTOCONFIGURE=0\n %sRscript -e \"\n source('libraries.R')\n%s\"", + env_exports, r_command ) } @@ -718,14 +739,16 @@ build_reticulate_phase <- function(r_command) { #' @param expr Symbol, Python object to be loaded into R. #' @param nix_env Character, path to the Nix environment file, default is #' "default.nix". +#' @param env_var Named list of environment variables to set. RETICULATE_AUTOCONFIGURE=0 +#' is set by default to prevent reticulate from modifying PYTHONPATH in Nix builds. #' @details `rxp_py2r(my_obj, my_python_object)` loads a serialized Python #' object and saves it as an RDS file using `reticulate::py_load_object()`. #' @return An object of class `rxp_derivation`. #' @export -rxp_py2r <- function(name, expr, nix_env = "default.nix") { +rxp_py2r <- function(name, expr, nix_env = "default.nix", env_var = NULL) { out_name <- deparse1(substitute(name)) expr_str <- deparse1(substitute(expr)) - rxp_common_setup(out_name, expr_str, nix_env, "py2r") + rxp_common_setup(out_name, expr_str, nix_env, "py2r", env_var) } #' Transfer R Object into a Python Session @@ -735,12 +758,14 @@ rxp_py2r <- function(name, expr, nix_env = "default.nix") { #' @param expr Symbol, R object to be saved into a Python pickle. #' @param nix_env Character, path to the Nix environment file, default is #' "default.nix". +#' @param env_var Named list of environment variables to set. RETICULATE_AUTOCONFIGURE=0 +#' is set by default to prevent reticulate from modifying PYTHONPATH in Nix builds. #' @details `rxp_r2py(my_obj, my_r_object)` saves an R object to a Python pickle #' using `reticulate::py_save_object()`. #' @return An object of class `rxp_derivation`. #' @export -rxp_r2py <- function(name, expr, nix_env = "default.nix") { +rxp_r2py <- function(name, expr, nix_env = "default.nix", env_var = NULL) { out_name <- deparse1(substitute(name)) expr_str <- deparse1(substitute(expr)) - rxp_common_setup(out_name, expr_str, nix_env, "r2py") + rxp_common_setup(out_name, expr_str, nix_env, "r2py", env_var) } diff --git a/R/rxp_make.R b/R/rxp_make.R index ad75388..6901c48 100644 --- a/R/rxp_make.R +++ b/R/rxp_make.R @@ -506,4 +506,4 @@ rxp_import_artifacts <- function( message("Importing store paths from ", archive_file) system2("nix-store", args = "--import", stdin = archive_file) message("Import completed") -} \ No newline at end of file +} diff --git a/man/rxp_py2r.Rd b/man/rxp_py2r.Rd index b0dc5db..211c366 100644 --- a/man/rxp_py2r.Rd +++ b/man/rxp_py2r.Rd @@ -4,7 +4,7 @@ \alias{rxp_py2r} \title{Transfer Python Object into an R Session} \usage{ -rxp_py2r(name, expr, nix_env = "default.nix") +rxp_py2r(name, expr, nix_env = "default.nix", env_var = NULL) } \arguments{ \item{name}{Symbol, name of the derivation.} @@ -13,6 +13,9 @@ rxp_py2r(name, expr, nix_env = "default.nix") \item{nix_env}{Character, path to the Nix environment file, default is "default.nix".} + +\item{env_var}{Named list of environment variables to set. RETICULATE_AUTOCONFIGURE=0 +is set by default to prevent reticulate from modifying PYTHONPATH in Nix builds.} } \value{ An object of class \code{rxp_derivation}. diff --git a/man/rxp_r2py.Rd b/man/rxp_r2py.Rd index b64af0c..2e7ab20 100644 --- a/man/rxp_r2py.Rd +++ b/man/rxp_r2py.Rd @@ -4,7 +4,7 @@ \alias{rxp_r2py} \title{Transfer R Object into a Python Session} \usage{ -rxp_r2py(name, expr, nix_env = "default.nix") +rxp_r2py(name, expr, nix_env = "default.nix", env_var = NULL) } \arguments{ \item{name}{Symbol, name of the derivation.} @@ -13,6 +13,9 @@ rxp_r2py(name, expr, nix_env = "default.nix") \item{nix_env}{Character, path to the Nix environment file, default is "default.nix".} + +\item{env_var}{Named list of environment variables to set. RETICULATE_AUTOCONFIGURE=0 +is set by default to prevent reticulate from modifying PYTHONPATH in Nix builds.} } \value{ An object of class \code{rxp_derivation}. diff --git a/tests/testthat/test-reticulate-config.R b/tests/testthat/test-reticulate-config.R new file mode 100644 index 0000000..233c087 --- /dev/null +++ b/tests/testthat/test-reticulate-config.R @@ -0,0 +1,67 @@ +test_that("rxp_py2r sets RETICULATE_AUTOCONFIGURE=0 by default", { + # Mock user input + nix_env <- "default.nix" + + # Default behavior + res <- rxp_py2r( + name = my_obj, + expr = py_obj, + nix_env = nix_env + ) + + expect_true(!is.null(res$env_var)) + expect_equal(res$env_var[["RETICULATE_AUTOCONFIGURE"]], "0") + + # Verify it appears in the snippet (indirectly validating build_reticulate_phase logic if it was simple string matching) + # But better to check the structured 'env_var' component we added. +}) + +test_that("rxp_r2py sets RETICULATE_AUTOCONFIGURE=0 by default", { + nix_env <- "default.nix" + + res <- rxp_r2py( + name = py_obj, + expr = r_obj, + nix_env = nix_env + ) + + expect_true(!is.null(res$env_var)) + expect_equal(res$env_var[["RETICULATE_AUTOCONFIGURE"]], "0") +}) + +test_that("User can override RETICULATE_AUTOCONFIGURE", { + nix_env <- "default.nix" + + # Override to "1" + res_override <- rxp_py2r( + name = my_obj, + expr = py_obj, + nix_env = nix_env, + env_var = c(RETICULATE_AUTOCONFIGURE = "1") + ) + + expect_equal(res_override$env_var[["RETICULATE_AUTOCONFIGURE"]], "1") + + # Override to empty string/something else + res_override2 <- rxp_r2py( + name = py_obj, + expr = r_obj, + nix_env = nix_env, + env_var = c(RETICULATE_AUTOCONFIGURE = "custom") + ) + expect_equal(res_override2$env_var[["RETICULATE_AUTOCONFIGURE"]], "custom") +}) + +test_that("env_var merging works correctly with other variables", { + nix_env <- "default.nix" + + res <- rxp_py2r( + name = my_obj, + expr = py_obj, + nix_env = nix_env, + env_var = c(OTHER_VAR = "foo") + ) + + expect_equal(res$env_var[["RETICULATE_AUTOCONFIGURE"]], "0") + expect_equal(res$env_var[["OTHER_VAR"]], "foo") +})