diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index a3ac6182..4fc93111 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -46,4 +46,5 @@ jobs: - uses: r-lib/actions/check-r-package@v2 with: + args: 'c("--as-cran")' upload-snapshots: true diff --git a/R/available_r.R b/R/available_r.R new file mode 100644 index 00000000..e3ef7d0d --- /dev/null +++ b/R/available_r.R @@ -0,0 +1,21 @@ +# WARNING - Generated by {fusen} from dev/flat_available_R.Rmd: do not edit by hand + +#' List available R versions from Nixpkgs +#' @return A character vector containing the available R versions. +#' @export +#' +#' @examples +#' available_r() +available_r <- function(){ + + temp <- new.env(parent = emptyenv()) + + data(list = "r_nix_revs", + package = "rix", + envir = temp) + + get("r_nix_revs", envir = temp) + + c("latest", r_nix_revs$version) +} + diff --git a/R/fetchgit.R b/R/fetchgit.R new file mode 100644 index 00000000..ea2addaf --- /dev/null +++ b/R/fetchgit.R @@ -0,0 +1,130 @@ +# WARNING - Generated by {fusen} from dev/flat_fetchers.Rmd: do not edit by hand + +#' fetchgit Downloads and installs a package hosted of Git +#' @param git_pkg A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. +#' @return A character. The Nix definition to download and build the R package from Github. +#' @noRd +fetchgit <- function(git_pkg){ + + package_name <- git_pkg$package_name + repo_url <- git_pkg$repo_url + branch_name <- git_pkg$branch_name + commit <- git_pkg$commit + + output <- get_sri_hash_deps(repo_url, branch_name, commit) + sri_hash <- output$sri_hash + imports <- output$deps + + sprintf('(pkgs.rPackages.buildRPackage { + name = \"%s\"; + src = pkgs.fetchgit { + url = \"%s\"; + branchName = \"%s\"; + rev = \"%s\"; + sha256 = \"%s\"; + }; + propagatedBuildInputs = builtins.attrValues { + inherit (pkgs.rPackages) %s; + }; + })', + package_name, + repo_url, + branch_name, + commit, + sri_hash, + imports +) + +} + + +#' fetchzip Downloads and installs an archived CRAN package +#' @param archive_pkg A character of the form "dplyr@0.80" +#' @return A character. The Nix definition to download and build the R package from CRAN. +#' @noRd +fetchzip <- function(archive_pkg, sri_hash = NULL){ + + pkgs <- unlist(strsplit(archive_pkg, split = "@")) + + cran_archive_link <- paste0( + "https://cran.r-project.org/src/contrib/Archive/", + pkgs[1], "/", + paste0(pkgs[1], "_", pkgs[2]), + ".tar.gz") + + package_name <- pkgs[1] + repo_url <- cran_archive_link + + if(is.null(sri_hash)){ + output <- get_sri_hash_deps(repo_url, branch_name = NULL, commit = NULL) + sri_hash <- output$sri_hash + imports <- output$deps + } else { + sri_hash <- sri_hash + imports <- NULL + } + + sprintf('(pkgs.rPackages.buildRPackage { + name = \"%s\"; + src = pkgs.fetchzip { + url = \"%s\"; + sha256 = \"%s\"; + }; + propagatedBuildInputs = builtins.attrValues { + inherit (pkgs.rPackages) %s; + }; + })', + package_name, + repo_url, + sri_hash, + imports +) +} + + + +#' fetchgits Downloads and installs a packages hosted of Git. Wraps `fetchgit()` to handle multiple packages +#' @param git_pkgs A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. This argument can also be a list of lists of these four elements. +#' @return A character. The Nix definition to download and build the R package from Github. +#' @noRd +fetchgits <- function(git_pkgs){ + + if(!all(sapply(git_pkgs, is.list))){ + fetchgit(git_pkgs) + } else if(all(sapply(git_pkgs, is.list))){ + paste(lapply(git_pkgs, fetchgit), collapse = "\n") + } else { + stop("There is something wrong with the input. Make sure it is either a list of four elements 'package_name', 'repo_url', 'branch_name' and 'commit' or a list of lists with these four elements") + } + +} + +#' fetchzips Downloads and installs packages hosted in the CRAN archives. Wraps `fetchzip()` to handle multiple packages. +#' @param archive_pkgs A character, or an atomic vector of characters. +#' @return A character. The Nix definition to download and build the R package from the CRAN archives. +#' @noRd +fetchzips <- function(archive_pkgs){ + + if(is.null(archive_pkgs)){ + "" #Empty character in case the user doesn't need any packages from the CRAN archives. + } else if(length(archive_pkgs) == 1){ + fetchzip(archive_pkgs) + } else if(length(archive_pkgs) > 1){ + paste(lapply(archive_pkgs, fetchzip), collapse = "\n") + } else { + stop("There is something wrong with the input. Make sure it is either a sinle package name, or an atomic vector of package names, for example c('dplyr@0.8.0', 'tidyr@1.0.0').") + } + +} + +#' fetchpkgs Downloads and installs packages hosted in the CRAN archives or Github. +#' @param git_pkgs A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. This argument can also be a list of lists of these four elements. +#' @param archive_pkgs A character, or an atomic vector of characters. +#' @return A character. The Nix definition to download and build the R package from the CRAN archives. +#' @noRd +fetchpkgs <- function(git_pkgs, archive_pkgs){ + paste(fetchgits(git_pkgs), + fetchzips(archive_pkgs), + collapse = "\n") +} + diff --git a/R/find_rev.R b/R/find_rev.R index eaf01377..e66cde35 100644 --- a/R/find_rev.R +++ b/R/find_rev.R @@ -1,4 +1,4 @@ -# WARNING - Generated by {fusen} from dev/flat_build_envs.Rmd: do not edit by hand +# WARNING - Generated by {fusen} from dev/flat_find_rev.Rmd: do not edit by hand #' find_rev Find the right Nix revision #' @param r_version Character. R version to look for, for example, "4.2.0". If a nixpkgs revision is provided instead, this gets returned. @@ -34,1859 +34,3 @@ find_rev <- function(r_version) { } - -#' List available R versions from Nixpkgs -#' @return A character vector containing the available R versions. -#' @export -#' -#' @examples -#' available_r() -available_r <- function(){ - - temp <- new.env(parent = emptyenv()) - - data(list = "r_nix_revs", - package = "rix", - envir = temp) - - get("r_nix_revs", envir = temp) - - c("latest", r_nix_revs$version) -} - - -#' get_latest Get the latest R version and packages -#' @return A character. The commit hash of the latest nixpkgs-unstable revision -#' @importFrom httr content GET stop_for_status -#' @importFrom jsonlite fromJSON -#' -#' @noRd -get_latest <- function() { - api_url <- "https://api.github.com/repos/NixOS/nixpkgs/commits?sha=nixpkgs-unstable" - - tryCatch({ - response <- httr::GET(url = api_url) - httr::stop_for_status(response) - commit_data <- jsonlite::fromJSON(httr::content(response, "text")) - latest_commit <- commit_data$sha[1] - return(latest_commit) - }, error = function(e) { - cat("Error:", e$message, "\n") - return(NULL) - }) -} - -#' get_sri_hash_deps Get the SRI hash of the NAR serialization of a Github repo -#' @param repo_url A character. The URL to the package's Github repository or to the `.tar.gz` package hosted on CRAN. -#' @param branch_name A character. The branch of interest, NULL for archived CRAN packages. -#' @param commit A character. The commit hash of interest, for reproducibility's sake, NULL for archived CRAN packages. -#' @importFrom httr content GET http_error -#' @return The SRI hash as a character -#' @noRd -get_sri_hash_deps <- function(repo_url, branch_name, commit){ - result <- httr::GET(paste0("http://git2nixsha.dev:1506/hash?repo_url=", - repo_url, - "&branchName=", - branch_name, - "&commit=", - commit)) - - if(http_error(result)){ - stop(paste0("Error in pulling URL: ", repo_url, ". If it's a Github repo, check the url, branch name and commit. Are these correct? If it's an archived CRAN package, check the name of the package and the version number.")) - } - - - lapply(httr::content(result), unlist) - -} - -#' fetchgit Downloads and installs a package hosted of Git -#' @param git_pkg A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. -#' @return A character. The Nix definition to download and build the R package from Github. -#' @noRd -fetchgit <- function(git_pkg){ - - package_name <- git_pkg$package_name - repo_url <- git_pkg$repo_url - branch_name <- git_pkg$branch_name - commit <- git_pkg$commit - - output <- get_sri_hash_deps(repo_url, branch_name, commit) - sri_hash <- output$sri_hash - imports <- output$deps - - sprintf('(pkgs.rPackages.buildRPackage { - name = \"%s\"; - src = pkgs.fetchgit { - url = \"%s\"; - branchName = \"%s\"; - rev = \"%s\"; - sha256 = \"%s\"; - }; - propagatedBuildInputs = builtins.attrValues { - inherit (pkgs.rPackages) %s; - }; - })', - package_name, - repo_url, - branch_name, - commit, - sri_hash, - imports -) - -} - - -#' fetchzip Downloads and installs an archived CRAN package -#' @param archive_pkg A character of the form "dplyr@0.80" -#' @return A character. The Nix definition to download and build the R package from CRAN. -#' @noRd -fetchzip <- function(archive_pkg, sri_hash = NULL){ - - pkgs <- unlist(strsplit(archive_pkg, split = "@")) - - cran_archive_link <- paste0( - "https://cran.r-project.org/src/contrib/Archive/", - pkgs[1], "/", - paste0(pkgs[1], "_", pkgs[2]), - ".tar.gz") - - package_name <- pkgs[1] - repo_url <- cran_archive_link - - if(is.null(sri_hash)){ - output <- get_sri_hash_deps(repo_url, branch_name = NULL, commit = NULL) - sri_hash <- output$sri_hash - imports <- output$deps - } else { - sri_hash <- sri_hash - imports <- NULL - } - - sprintf('(pkgs.rPackages.buildRPackage { - name = \"%s\"; - src = pkgs.fetchzip { - url = \"%s\"; - sha256 = \"%s\"; - }; - propagatedBuildInputs = builtins.attrValues { - inherit (pkgs.rPackages) %s; - }; - })', - package_name, - repo_url, - sri_hash, - imports -) -} - - - -#' fetchgits Downloads and installs a packages hosted of Git. Wraps `fetchgit()` to handle multiple packages -#' @param git_pkgs A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. This argument can also be a list of lists of these four elements. -#' @return A character. The Nix definition to download and build the R package from Github. -#' @noRd -fetchgits <- function(git_pkgs){ - - if(!all(sapply(git_pkgs, is.list))){ - fetchgit(git_pkgs) - } else if(all(sapply(git_pkgs, is.list))){ - paste(lapply(git_pkgs, fetchgit), collapse = "\n") - } else { - stop("There is something wrong with the input. Make sure it is either a list of four elements 'package_name', 'repo_url', 'branch_name' and 'commit' or a list of lists with these four elements") - } - -} - -#' fetchzips Downloads and installs packages hosted in the CRAN archives. Wraps `fetchzip()` to handle multiple packages. -#' @param archive_pkgs A character, or an atomic vector of characters. -#' @return A character. The Nix definition to download and build the R package from the CRAN archives. -#' @noRd -fetchzips <- function(archive_pkgs){ - - if(is.null(archive_pkgs)){ - "" #Empty character in case the user doesn't need any packages from the CRAN archives. - } else if(length(archive_pkgs) == 1){ - fetchzip(archive_pkgs) - } else if(length(archive_pkgs) > 1){ - paste(lapply(archive_pkgs, fetchzip), collapse = "\n") - } else { - stop("There is something wrong with the input. Make sure it is either a sinle package name, or an atomic vector of package names, for example c('dplyr@0.8.0', 'tidyr@1.0.0').") - } - -} - -#' fetchpkgs Downloads and installs packages hosted in the CRAN archives or Github. -#' @param git_pkgs A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. This argument can also be a list of lists of these four elements. -#' @param archive_pkgs A character, or an atomic vector of characters. -#' @return A character. The Nix definition to download and build the R package from the CRAN archives. -#' @noRd -fetchpkgs <- function(git_pkgs, archive_pkgs){ - paste(fetchgits(git_pkgs), - fetchzips(archive_pkgs), - collapse = "\n") -} - - -#' rix Generates a Nix expression that builds a reproducible development environment -#' @return Nothing, this function only has the side-effect of writing a file -#' called "default.nix" in the working directory. This file contains the -#' expression to build a reproducible environment using the Nix package -#' manager. -#' @param r_ver Character, defaults to "latest". The required R version, for example "4.0.0". -#' To use the latest version of R, use "latest", if you need the latest, bleeding edge version -#' of R and packages, then use "latest". You can check which R versions are available using `available_r`. -#' For reproducibility purposes, you can also provide a nixpkgs revision. -#' @param r_pkgs Vector of characters. List the required R packages for your -#' analysis here. -#' @param system_pkgs Vector of characters. List further software you wish to install that -#' are not R packages such as command line applications for example. -#' @param git_pkgs List. A list of packages to install from Git. See details for more information. -#' @param tex_pkgs Vector of characters. A set of tex packages to install. Use this if you need to compile `.tex` documents, or build PDF documents using Quarto. If you don't know which package to add, start by adding "amsmath". See the Vignette "Authoring LaTeX documents" for more details. -#' @param ide Character, defaults to "other". If you wish to use RStudio to work -#' interactively use "rstudio" or "code" for Visual Studio Code. For other editors, -#' use "other". This has been tested with RStudio, VS Code and Emacs. If other -#' editors don't work, please open an issue. -#' @param project_path Character, defaults to the current working directory. Where to write -#' `default.nix`, for example "/home/path/to/project". -#' The file will thus be written to the file "/home/path/to/project/default.nix". -#' @param overwrite Logical, defaults to FALSE. If TRUE, overwrite the `default.nix` -#' file in the specified path. -#' @param print Logical, defaults to FALSE. If TRUE, print `default.nix` to console. -#' @param shell_hook Character, defaults to `"R --vanilla"`. Commands added to the shell_hook get -#' executed when the Nix shell starts (via `shellHook`). So by default, using `nix-shell default.nix` will -#' start R. Set to NULL if you want bash to be started instead. -#' @details This function will write a `default.nix` in the chosen path. Using -#' the Nix package manager, it is then possible to build a reproducible -#' development environment using the `nix-build` command in the path. This -#' environment will contain the chosen version of R and packages, and will not -#' interfere with any other installed version (via Nix or not) on your -#' machine. Every dependency, including both R package dependencies but also -#' system dependencies like compilers will get installed as well in that -#' environment. If you use RStudio for interactive work, then set the -#' `rstudio` parameter to `TRUE`. If you use another IDE (for example Emacs or -#' Visual Studio Code), you do not need to add it to the `default.nix` file, -#' you can simply use the version that is installed on your computer. Once you built -#' the environment using `nix-build`, you can drop into an interactive session -#' using `nix-shell`. See the "Building reproducible development environments with rix" -#' vignette for detailled instructions. -#' Packages to install from Github must be provided in a list of 4 elements: -#' "package_name", "repo_url", "branch_name" and "commit". -#' This argument can also be a list of lists of these 4 elements. It is also possible to install old versions -#' of packages by specifying a version. For example, to install the latest -#' version of `{AER}` but an old version of `{ggplot2}`, you could -#' write: `r_pkgs = c("AER", "ggplot2@2.2.1")`. Note -#' however that doing this could result in dependency hell, because an older -#' version of a package might need older versions of its dependencies, but other -#' packages might need more recent versions of the same dependencies. If instead you -#' want to use an environment as it would have looked at the time of `{ggplot2}`'s -#' version 2.2.1 release, then use the Nix revision closest to that date, by setting -#' `r_ver = "3.1.0"`, which was the version of R current at the time. This -#' ensures that Nix builds a completely coherent environment. -#' By default, the nix shell will be configured with `"en_US.UTF-8"` for the -#' relevant locale variables (`LANG`, `LC_ALL`, `LC_TIME`, `LC_MONETARY`, -#' `LC_PAPER`, `LC_MEASUREMENT`). This is done to ensure locale -#' reproducibility by default in Nix environments created with `rix()`. -#' If there are good reasons to not stick to the default, you can set your -#' preferred locale variables via -#' `options(rix.nix_locale_variables = list(LANG = "de_CH.UTF-8", <...>)` -#' and the aforementioned locale variable names. -#' @export -#' @examples -#' \dontrun{ -#' # Build an environment with the latest version of R -#' # and the dplyr and ggplot2 packages -#' rix(r_ver = "latest", -#' r_pkgs = c("dplyr", "ggplot2"), -#' system_pkgs = NULL, -#' git_pkgs = NULL, -#' ide = "code", -#' project_path = path_default_nix, -#' overwrite = TRUE, -#' print = TRUE) -#' } -rix <- function(r_ver = "latest", - r_pkgs = NULL, - system_pkgs = NULL, - git_pkgs = NULL, - tex_pkgs = NULL, - ide = "other", - project_path = ".", - overwrite = FALSE, - print = FALSE, - shell_hook = "R --vanilla"){ - - stopifnot("'ide' has to be one of 'other', 'rstudio' or 'code'" = (ide %in% c("other", "rstudio", "code"))) - - project_path <- if(project_path == "."){ - "default.nix" - } else { - paste0(project_path, "/default.nix") - } - - # Generate the correct text for the header depending on wether - # an R version or a Nix revision is supplied to `r_ver` - if(nchar(r_ver) > 20){ - r_ver_text <- paste0("as it was as of nixpkgs revision: ", r_ver) - } else { - r_ver_text <- paste0("version ", r_ver) - } - - # Find the Nix revision to use - nix_revision <- find_rev(r_ver) - - project_path <- file.path(project_path) - - rix_call <- match.call() - - generate_rix_call <- function(rix_call, nix_revision){ - - rix_call$r_ver <- nix_revision - - rix_call <- paste0("# >", deparse1(rix_call)) - - gsub(",", ",\n# >", rix_call) - } - - # Get the rix version - rix_version <- utils::packageVersion("rix") - - generate_header <- function(rix_version, - nix_revision, - r_ver_text, - rix_call){ - - if(identical(Sys.getenv("TESTTHAT"), "true")){ - sprintf(' -let - pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/%s.tar.gz") {}; -', -nix_revision) - } else { - sprintf('# This file was generated by the {rix} R package v%s on %s -# with following call: -%s -# It uses nixpkgs\' revision %s for reproducibility purposes -# which will install R %s -# Report any issues to https://github.com/b-rodrigues/rix -let - pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/%s.tar.gz") {}; -', -rix_version, -Sys.Date(), -generate_rix_call(rix_call, nix_revision), -nix_revision, -r_ver_text, -nix_revision -) - } - - } - - # Now we need to generate all the different sets of packages - # to install. Let's start by the CRAN packages, current - # and archived. The function below builds the strings. - get_rPackages <- function(r_pkgs){ - - # in case users pass something like c("dplyr", "tidyr@1.0.0") - # r_pkgs will be "dplyr" only - # and "tidyr@1.0.0" needs to be handled by fetchzips - r_and_archive_pkgs <- detect_versions(r_pkgs) - - # overwrite r_pkgs - r_pkgs <- r_and_archive_pkgs$cran_packages - - # get archive_pkgs - archive_pkgs <- r_and_archive_pkgs$archive_packages - - r_pkgs <- if(ide == "code"){ - c(r_pkgs, "languageserver") - } else { - r_pkgs - } - - rPackages <- paste(r_pkgs, collapse = ' ') - - rPackages <- gsub('\\.', '_', rPackages) - - list("rPackages" = rPackages, - "archive_pkgs" = archive_pkgs) - - } - - # Get the two lists. One list is current CRAN packages - # the other is archived CRAN packages. - cran_pkgs <- get_rPackages(r_pkgs) - - # we need to know if the user wants R packages - - flag_rpkgs <- if(is.null(cran_pkgs$rPackages) | cran_pkgs$rPackages == ""){ - "" - } else { - "rpkgs" - } - - # generate_* function generate the actual Nix code - generate_rpkgs <- function(rPackages) { - if (flag_rpkgs == ""){ - NULL - } else { - sprintf('rpkgs = builtins.attrValues { - inherit (pkgs.rPackages) %s; -}; -', -rPackages) - } - } - - # Texlive packages - generate_tex_pkgs <- function(tex_pkgs) { - if (!is.null(tex_pkgs)) { - - tex_pkgs <- paste(tex_pkgs, collapse = ' ') - - sprintf('tex = (pkgs.texlive.combine { - inherit (pkgs.texlive) scheme-small %s; -}); -', -tex_pkgs) - } - } - - flag_tex_pkgs <- if(is.null(tex_pkgs)){ - "" - } else { - "tex" - } - - # system packages - get_system_pkgs <- function(system_pkgs, r_pkgs){ - - system_pkgs <- if(any(grepl("quarto", r_pkgs))){ - unique(c(system_pkgs, "quarto")) - } else { - system_pkgs - } - - paste(system_pkgs, collapse = ' ') - } - - flag_git_archive <- if(!is.null(cran_pkgs$archive) | !is.null(git_pkgs)){ - "git_archive_pkgs" - } else { - "" - } - - generate_git_archived_packages <- function(git_pkgs, archive_pkgs){ - if(flag_git_archive == ""){ - NULL - } else { - sprintf('git_archive_pkgs = [%s];\n', - fetchpkgs(git_pkgs, archive_pkgs) - ) - } - } - - - # `R` needs to be added. If we were using the rWrapper - # this wouldn't be needed, but we're not so we need - # to add it. - generate_system_pkgs <- function(system_pkgs, r_pkgs){ - sprintf('system_packages = builtins.attrValues { - inherit (pkgs) R glibcLocales nix %s; -}; -', -get_system_pkgs(system_pkgs, r_pkgs)) - } - - generate_locale_variables <- function() { - locale_defaults <- list( - LANG = "en_US.UTF-8", - LC_ALL = "en_US.UTF-8", - LC_TIME = "en_US.UTF-8", - LC_MONETARY = "en_US.UTF-8", - LC_PAPER = "en_US.UTF-8", - LC_MEASUREMENT = "en_US.UTF-8" - ) - locale_variables <- getOption( - "rix.nix_locale_variables", - default = locale_defaults - ) - valid_vars <- all(names(locale_variables) %in% names(locale_defaults)) - if (!isTRUE(valid_vars)) { - stop("`options(rix.nix_locale_variables = list())` ", - "only allows the following element names (locale variables):\n", - paste(names(locale_defaults), collapse = "; "), - call. = FALSE) - } - locale_vars <- paste( - Map(function(x, nm) paste0(nm, ' = ', '"', x, '"'), - nm = names(locale_variables), x = locale_variables), - collapse = ";\n " - ) - paste0(locale_vars, ";\n") - } - - generate_rstudio_pkgs <- function(ide, flag_git_archive, flag_rpkgs){ - if(ide == "rstudio"){ - sprintf('rstudio_pkgs = pkgs.rstudioWrapper.override { - packages = [ %s %s ]; -}; -', -flag_git_archive, -flag_rpkgs -) - } else { - NULL - } - } - - flag_rstudio <- if (ide == "rstudio") "rstudio_pkgs" else "" - - shell_hook <- if (!is.null(shell_hook) && nzchar(shell_hook)) { - paste0('shellHook = "', shell_hook, '";') - } else {''} - - # Generate the shell - generate_shell <- function(flag_git_archive, - flag_rpkgs){ - sprintf('in - pkgs.mkShell { - %s - %s - buildInputs = [ %s %s %s system_packages %s ]; - %s - }', - generate_locale_archive(detect_os()), - generate_locale_variables(), - flag_git_archive, - flag_rpkgs, - flag_tex_pkgs, - flag_rstudio, - shell_hook - ) - - } - - # Generate default.nix file - default.nix <- paste( - generate_header(rix_version, - nix_revision, - r_ver_text, - rix_call), - generate_rpkgs(cran_pkgs$rPackages), - generate_git_archived_packages(git_pkgs, cran_pkgs$archive_pkgs), - generate_tex_pkgs(tex_pkgs), - generate_system_pkgs(system_pkgs, r_pkgs), - generate_rstudio_pkgs(ide, flag_git_archive, flag_rpkgs), - generate_shell(flag_git_archive, flag_rpkgs), - collapse = "\n" - ) - - default.nix <- readLines(textConnection(default.nix)) - - if(print){ - cat(default.nix, sep = "\n") - } - - if(!file.exists(project_path) || overwrite){ - writeLines(default.nix, project_path) - } else { - stop(paste0("File exists at ", project_path, ". Set `overwrite == TRUE` to overwrite.")) - } - - - -} - -#' @noRd -create_default_nix <- function(path = file.path("inst", "extdata", - "default.nix")) { - if (!dir.exists(dirname(path))) { - stop("Path", path, " does not exist.") - } - - rix( - r_ver = "latest", - r_pkgs = NULL, - system_pkgs = NULL, - git_pkgs = list( - list( - package_name = "rix", - repo_url = "https://github.com/b-rodrigues/rix", - branch_name = "master", - commit = "22711ee98c0092e56c620122800ca8f30b773a65" - ) - ), - ide = "other", - project_path = dirname(path), - overwrite = TRUE, - shell_hook = "R --vanilla" - ) -} - -#' Invoke shell command `nix-build` from an R session -#' @param project_path Path to the folder where the `default.nix` file resides. -#' The default is `"."`, which is the working directory in the current R -#' session. -#' @param exec_mode Either `"blocking"` (default) or `"non-blocking`. This -#' will either block the R session while the `nix-build` shell command is -#' executed, or run `nix-build` in the background ("non-blocking"). -#' @return integer of the process ID (PID) of `nix-build` shell command -#' launched, if `nix_build()` call is assigned to an R object. Otherwise, it -#' will be returned invisibly. -#' @details The `nix-build` command line interface has more arguments. We will -#' probably not support all of them in this R wrapper, but currently we have -#' support for the following `nix-build` flags: -#' - `--max-jobs`: Maximum number of build jobs done in parallel by Nix. -#' According to the official docs of Nix, it defaults to `1`, which is one -#' core. This option can be useful for shared memory multiprocessing or -#' systems with high I/O latency. To set `--max-jobs` used, you can declare -#' with `options(rix.nix_build_max_jobs = )`. Once you call -#' `nix_build()` the flag will be propagated to the call of `nix-build`. -#' @export -#' @examples -#' \dontrun{ -#' nix_build() -#' } -nix_build <- function(project_path = ".", - exec_mode = c("blocking", "non-blocking")) { - has_nix_build <- nix_build_installed() # TRUE if yes, FALSE if no - nix_file <- file.path(project_path, "default.nix") - - stopifnot( - "`project_path` must be character of length 1." = - is.character(project_path) && length(project_path) == 1L, - "`project_path` has no `default.nix` file. Use one that contains `default.nix`" = - file.exists(nix_file), - "`nix-build` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = - isTRUE(has_nix_build) - ) - exec_mode <- match.arg(exec_mode) - - max_jobs <- getOption("rix.nix_build_max_jobs", default = 1L) - stopifnot("option `rix.nix_build_max_jobs` is not integerish" = - is_integerish(max_jobs)) - max_jobs <- as.integer(max_jobs) - - if (max_jobs == 1L) { - cmd <- c("nix-build", nix_file) - } else { - cmd <- c("nix-build", "--max-jobs", as.character(max_jobs), nix_file) - } - - cat(paste0("Launching `", paste0(cmd, collapse = " "), "`", " in ", - exec_mode, " mode\n")) - - proc <- switch(exec_mode, - "blocking" = sys::exec_internal(cmd = cmd), - "non-blocking" = sys::exec_background(cmd = cmd), - stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') - ) - - if (exec_mode == "non-blocking") { - poll_sys_proc_nonblocking(cmd, proc, what = "nix-build") - } else if (exec_mode == "blocking") { - poll_sys_proc_blocking(cmd, proc, what = "nix-build") - } - - # todo (?): clean zombies for background/non-blocking mode - - return(invisible(proc)) -} - -#' @noRd -poll_sys_proc_blocking <- function(cmd, proc, - what = c("nix-build", "expr")) { - what <- match.arg(what) - status <- proc$status - if (status == 0L) { - cat(paste0("\n==> ", sys::as_text(proc$stdout))) - cat(paste0("\n==> `", what, "` succeeded!")) - } else { - msg <- nix_build_exit_msg() - cat(paste0("`", cmd, "`", " failed with ", msg)) - } - return(invisible(status)) -} - -#' @noRd -poll_sys_proc_nonblocking <- function(cmd, proc, - what = c("nix-build", "expr")) { - what <- match.arg(what) - cat(paste0("\n==> Process ID (PID) is ", proc, ".")) - cat("\n==> Receiving stdout and stderr streams...\n") - status <- sys::exec_status(proc, wait = TRUE) - if (status == 0L) { - cat(paste0("\n==> `", what, "` succeeded!")) - } - return(invisible(status)) -} - -#' @noRd -is_integerish <- function(x, tol = .Machine$double.eps^0.5) { - return(abs(x - round(x)) < tol) -} - -#' @noRd -nix_build_installed <- function() { - exit_code <- system2("command", "-v", "nix-build") - if (exit_code == 0L) { - return(invisible(TRUE)) - } else { - return(invisible(FALSE)) - } -} - -#' @noRd -nix_build_exit_msg <- function(x) { - x_char <- as.character(x) - - err_msg <- switch( - x_char, - "100" = "generic build failure (100).", - "101" = "build timeout (101).", - "102" = "hash mismatch (102).", - "104" = "not deterministic (104).", - stop(paste0("general exit code ", x_char, ".")) - ) - - return(err_msg) -} - -#' Initiate and maintain an isolated, project-specific, and runtime-pure R -#' setup via Nix. -#' -#' Creates an isolated project folder for a Nix-R configuration. `rix::rix_init()` -#' also adds, appends, or updates with or without backup a custom `.Rprofile` -#' file with code that initializes a startup R environment without system's user -#' libraries within a Nix software environment. Instead, it restricts search -#' paths to load R packages exclusively from the Nix store. Additionally, it -#' makes Nix utilities like `nix-shell` available to run system commands from -#' the system's RStudio R session, for both Linux and macOS. -#' -#' **Enhancement of computational reproducibility for Nix-R environments:** -#' -#' The primary goal of `rix::rix_init()` is to enhance the computational -#' reproducibility of Nix-R environments during runtime. Notably, no restart is -#' required as environmental variables are set in the current session, in -#' addition to writing an `.Rprofile` file. This is particularly useful to make -#' [rix::with_nix()] evaluate custom R functions from any "Nix-to-Nix" or -#' "System-to-Nix" R setups. It introduces two side-effects that -#' take effect both in a current or later R session setup: -#' -#' 1. **Adjusting `R_LIBS_USER` path:** -#' By default, the first path of `R_LIBS_USER` points to the user library -#' outside the Nix store (see also [base::.libPaths()]). This creates -#' friction and potential impurity as R packages from the system's R user -#' library are loaded. While this feature can be useful for interactively -#' testing an R package in a Nix environment before adding it to a `.nix` -#' configuration, it can have undesired effects if not managed carefully. -#' A major drawback is that all R packages in the `R_LIBS_USER` location need -#' to be cleaned to avoid loading packages outside the Nix configuration. -#' Issues, especially on macOS, may arise due to segmentation faults or -#' incompatible linked system libraries. These problems can also occur -#' if one of the (reverse) dependencies of an R package is loaded along the -#' process. -#' -#' 2. **Make Nix commands available when running system commands from RStudio:** -#' In a host RStudio session not launched via Nix (`nix-shell`), the -#' environmental variables from `~/.zshrc` or `~/.bashrc` may not be -#' inherited. Consequently, Nix command line interfaces like `nix-shell` -#' might not be found. The `.Rprofile` code written by `rix::rix_init()` ensures -#' that Nix command line programs are accessible by adding the path of the -#' "bin" directory of the default Nix profile, -#' `"/nix/var/nix/profiles/default/bin"`, to the `PATH` variable in an -#' RStudio R session. -#' -#' These side effects are particularly recommended when working in flexible R -#' environments, especially for users who want to maintain both the system's -#' native R setup and utilize Nix expressions for reproducible development -#' environments. This init configuration is considered pivotal to enhance the -#' adoption of Nix in the R community, particularly until RStudio in Nixpkgs is -#' packaged for macOS. We recommend calling `rix::rix_init()` prior to comparing R -#' code ran between two software environments with `rix::with_nix()`. -#' -#' @param project_path Character with the folder path to the isolated nix-R project. -#' Defaults to `"."`, which is the current working directory path. If the folder -#' does not exist yet, it will be created. -#' @param rprofile_action Character. Action to take with `.Rprofile` file -#' destined for `project_path` folder. Possible values include -#' `"create_missing"`, which only writes `.Rprofile` if it -#' does not yet exist (otherwise does nothing); `"create_backup"`, which copies -#' the existing `.Rprofile` to a new backup file, generating names with -#' POSIXct-derived strings that include the time zone information. A new -#' `.Rprofile` file will be written with default code from `rix::rix_init()`; -#' `"overwrite"` overwrites the `.Rprofile` file if it does exist; `"append"` -#' appends the existing file with code that is tailored to an isolated Nix-R -#' project setup. -#' @param message_type Character. Message type, defaults to `"simple"`, which -#' gives minimal but sufficient feedback. Other values are currently -#' `"verbose"`, which provides more detailed diagnostics. -#' @export -#' @seealso [with_nix()] -#' @return Nothing, this function only has the side-effect of writing a file -#' called ".Rprofile" to the specified path. -#' @examples -#' \dontrun{ -#' # create an isolated, runtime-pure R setup via Nix -#' project_path <- "./sub_shell" -#' rix_init( -#' project_path = project_path, -#' rprofile_action = "create_missing" -#' ) -#' } -rix_init <- function(project_path = ".", - rprofile_action = c("create_missing", "create_backup", - "overwrite", "append"), - message_type = c("simple", "verbose")) { - message_type <- match.arg(message_type, choices = c("simple", "verbose")) - rprofile_action <- match.arg(rprofile_action, - choices = c("create_missing", "create_backup", "overwrite", "append")) - stopifnot( - "`project_path` needs to be character of length 1" = - is.character(project_path) && length(project_path) == 1L - ) - - cat("\n### Bootstrapping isolated, project-specific, and runtime-pure", - "R setup via Nix ###\n\n") - if (isFALSE(dir.exists(project_path))) { - dir.create(path = project_path, recursive = TRUE) - project_path <- normalizePath(path = project_path) - cat("==> Created isolated nix-R project folder:\n", project_path, "\n") - } else { - project_path <- normalizePath(path = project_path) - cat("==> Existing isolated nix-R project folder:\n", project_path, - "\n") - } - - # create project-local `.Rprofile` with pure settings - # first create the call, deparse it, and write it to .Rprofile - rprofile_quoted <- nix_rprofile() - rprofile_deparsed <- deparse_chr1(expr = rprofile_quoted, collapse = "\n") - rprofile_file <- file.path(project_path, ".Rprofile") - - rprofile_text <- get_rprofile_text(rprofile_deparsed) - write_rprofile <- function(rprofile_text, rprofile_file) { - writeLines( - text = rprofile_text, - con = file(rprofile_file) - ) - } - - is_nixr <- is_nix_rsession() - is_rstudio <- is_rstudio_session() - - rprofile_exists <- file.exists(rprofile_file) - timestamp <- format(Sys.time(), "%Y-%m-%dT%H:%M:%S%z") - rprofile_backup <- paste0(rprofile_file, "_backup_", timestamp) - - switch(rprofile_action, - create_missing = { - if (isTRUE(rprofile_exists)) { - cat( - "\n* Keep existing `.Rprofile`. in `project_path`:\n", - paste0(project_path, "/"), "\n" - ) - } else { - write_rprofile(rprofile_text, rprofile_file) - message_rprofile(action_string = "Added", project_path = project_path) - } - set_message_session_PATH(message_type = message_type) - }, - create_backup = { - if (isTRUE(rprofile_exists)) { - file.copy(from = rprofile_file, to = rprofile_backup) - cat( - "\n==> Backed up existing `.Rprofile` in file:\n", rprofile_backup, - "\n" - ) - write_rprofile(rprofile_text, rprofile_file) - message_rprofile( - action_string = "Overwrote", - project_path = project_path - ) - if (message_type == "verbose") { - cat("\n* Current lines of local `.Rprofile` are\n:") - cat(readLines(con = file(rprofile_file)), sep = "\n") - } - set_message_session_PATH(message_type = message_type) - } - }, - overwrite = { - write_rprofile(rprofile_text, rprofile_file) - if (isTRUE(rprofile_exists)) { - message_rprofile( - action_string = "Overwrote", project_path = project_path - ) - } else { - message_rprofile( - action_string = "Added", project_path = project_path - ) - } - }, - append = { - cat(paste0(rprofile_text, "\n"), file = rprofile_file, append = TRUE) - message_rprofile( - action_string = "Appended", project_path = project_path - ) - } - ) - - if (message_type == "verbose") { - cat("\n* Current lines of local `.Rprofile` are:\n\n") - cat(readLines(con = file(rprofile_file)), sep = "\n") - } - - on.exit(close(file(rprofile_file))) -} - -#' @noRd -get_rprofile_text <- function(rprofile_deparsed) { - c( -"### File generated by `rix::rix_init()` ### -# 1. Currently, system RStudio does not inherit environmental variables -# defined in `$HOME/.zshrc`, `$HOME/.bashrc` and alike. This is workaround to -# make the path of the nix store and hence basic nix commands available -# in an RStudio session -# 2. For nix-R session, remove `R_LIBS_USER`, system's R user library.`. -# This guarantees no user libraries from the system are loaded and only -# R packages in the Nix store are used. This makes Nix-R behave in pure manner -# at run-time.", - rprofile_deparsed - ) -} - -#' @noRd -message_rprofile <- function(action_string = "Added", - project_path = ".") { - msg <- paste0( - "\n==> ", action_string, - " `.Rprofile` file and code lines for new R sessions launched from:\n", - project_path, - "\n\n* Added the location of the Nix store to `PATH` ", - "environmental variable for new R sessions on host/docker RStudio:\n", - "/nix/var/nix/profiles/default/bin" - ) - cat(msg) -} - -#' @noRd -set_message_session_PATH <- function(message_type = c("simple", "verbose")) { - match.arg(message_type, choices = c("simple", "verbose")) - if (message_type == "verbose") { - cat("\n\n* Current `PATH` variable set in R session is:\n\n") - cat(Sys.getenv("PATH")) - } - cat("\n\n==> Also adjusting `PATH` via `Sys.setenv()`, so that", - "system commands can invoke key Nix commands like `nix-build` in this", - "RStudio session on the host operating system.") - PATH <- set_nix_path() - if (message_type == "verbose") { - cat("\n\n* Updated `PATH` variable is:\n\n", PATH) - } -} - -#' @noRd -is_nix_rsession <- function() { - is_nixr <- nzchar(Sys.getenv("NIX_STORE")) - if (isTRUE(is_nixr)) { - cat("==> R session running via Nix (nixpkgs)\n") - return(TRUE) - } else { - cat("\n==> R session running via host operating system or docker\n") - return(FALSE) - } -} - -#' @noRd -is_rstudio_session <- function() { - is_rstudio <- Sys.getenv("RSTUDIO") == "1" - if (isTRUE(is_rstudio)) { - cat("\n==> R session running from RStudio\n") - return(TRUE) - } else { - cat("* R session not running from RStudio") - return(FALSE) - } -} - -#' @noRd -set_nix_path <- function() { - old_path <- Sys.getenv("PATH") - nix_path <- "/nix/var/nix/profiles/default/bin" - has_nix_path <- any(grepl(nix_path, old_path)) - if (isFALSE(has_nix_path)) { - Sys.setenv( - PATH = paste(old_path, "/nix/var/nix/profiles/default/bin", sep = ":") - ) - } - invisible(Sys.getenv("PATH")) -} - -#' @noRd -nix_rprofile <- function() { - quote( { - is_rstudio <- Sys.getenv("RSTUDIO") == "1" - is_nixr <- nzchar(Sys.getenv("NIX_STORE")) - if (isFALSE(is_nixr) && isTRUE(is_rstudio)) { - # Currently, RStudio does not propagate environmental variables defined in - # `$HOME/.zshrc`, `$HOME/.bashrc` and alike. This is workaround to - # make the path of the nix store and hence basic nix commands available - # in an RStudio session - cat("{rix} detected RStudio R session") - old_path <- Sys.getenv("PATH") - nix_path <- "/nix/var/nix/profiles/default/bin" - has_nix_path <- any(grepl(nix_path, old_path)) - if (isFALSE(has_nix_path)) { - Sys.setenv( - PATH = paste( - old_path, nix_path, sep = ":" - ) - ) - } - rm(old_path, nix_path) - } - - if (isTRUE(is_nixr)) { - current_paths <- .libPaths() - userlib_paths <- Sys.getenv("R_LIBS_USER") - user_dir <- grep(paste(userlib_paths, collapse = "|"), current_paths) - new_paths <- current_paths[-user_dir] - # sets new library path without user library, making nix-R pure at - # run-time - .libPaths(new_paths) - rm(current_paths, userlib_paths, user_dir, new_paths) - } - - rm(is_rstudio, is_nixr) - } ) -} - - - -#' Evaluate function in R or shell command via `nix-shell` environment -#' -#' This function needs an installation of Nix. `with_nix()` has two effects -#' to run code in isolated and reproducible environments. -#' 1. Evaluate a function in R or a shell command via the `nix-shell` -#' environment (Nix expression for custom software libraries; involving pinned -#' versions of R and R packages via Nixpkgs) -#' 2. If no error, return the result object of `expr` in `with_nix()` into the -#' current R session. -#' -#' -#' -#' `with_nix()` gives you the power of evaluating a main function `expr` -#' and its function call stack that are defined in the current R session -#' in an encapsulated nix-R session defined by Nix expression (`default.nix`), -#' which is located in at a distinct project path (`project_path`). -#' -#' `with_nix()` is very convenient because it gives direct code feedback in -#' read-eval-print-loop style, which gives a direct interface to the very -#' reproducible infrastructure-as-code approach offered by Nix and Nixpkgs. You -#' don't need extra efforts such as setting up DevOps tooling like Docker and -#' domain specific tools like {renv} to control complex software environments in -#' R and any other language. It is for example useful for the following -#' purposes. -#' -#' 1. test compatibility of custom R code and software/package dependencies in -#' development and production environments -#' 2. directly stream outputs (returned objects), messages and errors from any -#' command line tool offered in Nixpkgs into an R session. -#' 3. Test if evolving R packages change their behavior for given unchanged -#' R code, and whether they give identical results or not. -#' -#' `with_nix()` can evaluate both R code from a nix-R session within -#' another nix-R session, and also from a host R session (i.e., on macOS or -#' Linux) within a specific nix-R session. This feature is useful for testing -#' the reproducibility and compatibility of given code across different software -#' environments. If testing of different sets of environments is necessary, you -#' can easily do so by providing Nix expressions in custom `.nix` or -#' `default.nix` files in different subfolders of the project. -#' -#' To do its job, `with_nix()` heavily relies on patterns that manipulate -#' language expressions (aka computing on the language) offered in base R as -#' well as the {codetools} package by Luke Tierney. -#' -#' Some of the key steps that are done behind the scene: -#' 1. recursively find, classify, and export global objects (globals) in the -#' call stack of `expr` as well as propagate R package environments found. -#' 2. Serialize (save to disk) and deserialize (read from disk) dependent -#' data structures as `.Rds` with necessary function arguments provided, -#' any relevant globals in the call stack, packages, and `expr` outputs -#' returned in a temporary directory. -#' 3. Use pure `nix-shell` environments to execute a R code script -#' reconstructed catching expressions with quoting; it is launched by commands -#' like this via `{sys}` by Jeroen Ooms: -#' `nix-shell --pure --run "Rscript --vanilla"`. -#' -#' @param expr Single R function or call, or character vector of length one with -#' shell command and possibly options (flags) of the command to be invoked. -#' For `program = R`, you can both use a named or an anonymous function. -#' The function provided in `expr` should not evaluate when you pass arguments, -#' hence you need to wrap your function call like -#' `function() your_fun(arg_a = "a", arg_b = "b")`, to avoid evaluation and make -#' sure `expr` is a function (see details and examples). -#' @param program String stating where to evaluate the expression. Either `"R"`, -#' the default, or `"shell"`. `where = "R"` will evaluate the expression via -#' `RScript` and `where = "shell"` will run the system command in `nix-shell`. -#' @param exec_mode Either `"blocking"` (default) or `"non-blocking`. This -#' will either block the R session while `expr` is running in a `nix-shell` -#' environment, oor running it in the background ("non-blocking"). While -#' `program = R` will yield identical results for foreground and background -#' evaluation (R object), `program = "shell"` will return list of exit status, -#' standard output and standard error of the system command and as text in -#' blocking mode. -#' @param project_path Path to the folder where the `default.nix` file resides. -#' The default is `"."`, which is the working directory in the current R -#' session. This approach also useful when you have different subfolders -#' with separate software environments defined in different `default.nix` files. -#' If you prefer to run code in custom `.nix` files in the same directory -#' using `with_nix()`, you can use the `nix_file` argument to specify paths -#' to `.nix` files. -#' @param nix_file Path to `.nix` file that contains the expressions defining -#' the Nix software environment in which you want to run `expr`. See -#' `project_path` argument as an alternative way to specify the environment. -#' @param message_type String how detailed output is. Currently, there is -#' either `"simple"` (default) or `"verbose"`, which shows the script that runs -#' via `nix-shell`. -#' @importFrom codetools findGlobals checkUsage -#' @export -#' @return -#' - if `program = "R"`, R object returned by function given in `expr` -#' when evaluated via the R environment in `nix-shell` defined by Nix -#' expression. -#' - if `program = "shell"`, list with the following elements: -#' - `status`: exit code -#' - `stdout`: character vector with standard output -#' - `stderr`: character vector with standard error -#' of `expr` command sent to a command line interface provided by a Nix package. -#' @examples -#' \dontrun{ -#' # create an isolated, runtime-pure R setup via Nix -#' project_path <- "./sub_shell" -#' rix_init( -#' project_path = project_path, -#' rprofile_action = "create_missing" -#' ) -#' # generate nix environment in `default.nix` -#' rix( -#' r_ver = "4.2.0", -#' project_path = project_path -#' ) -#' # evaluate function in Nix-R environment via `nix-shell` and `Rscript`, -#' # stream messages, and bring output back to current R session -#' out <- with_nix( -#' expr = function(mtcars) nrow(mtcars), -#' program = "R", exec_mode = "non-blocking", project_path = project_path, -#' message_type = "simple" -#' ) -#' -#' # There no limit in the complexity of function call stacks that `with_nix()` -#' # can possibly handle; however, `expr` should not evaluate and -#' # needs to be a function for `program = "R"`. If you want to pass the -#' # a function with arguments, you can do like this -#' get_sample <- function(seed, n) { -#' set.seed(seed) -#' out <- sample(seq(1, 10), n) -#' return(out) -#' } -#' -#' out <- with_nix( -#' expr = get_sample(seed = 1234, n = 5), -#' program = "R", exec_mode = "non-blocking", -#' project_path = ".", -#' message_type = "simple" -#' ) -#' -#' ## You can also use packages, which will be exported to the nix-R session -#' ## running through `nix-shell` environment -#' R 4.2.2 -#' } -with_nix <- function(expr, - program = c("R", "shell"), - exec_mode = c("blocking", "non-blocking"), - project_path = ".", - nix_file = NULL, - message_type = c("simple", "verbose")) { - if (is.null(nix_file)) { - nix_file <- file.path(project_path, "default.nix") - } - stopifnot( - "`project_path` must be character of length 1." = - is.character(project_path) && length(project_path) == 1L, - "`project_path` has no `default.nix` file. Use one that contains `default.nix`" = - file.exists(nix_file), - "`message_type` must be character." = is.character(message_type), - "`expr` needs to be a call or function for `program = R`, and character of length 1 for `program = shell`" = - is.function(expr) || is.call(expr) || (is.character(expr) && length(expr) == 1L) - ) - - # ad-hoc solution for RStudio's limitation that R sessions cannot yet inherit - # proper `PATH` from custom `.Rprofile` on macOS (2023-01-17) - # adjust `PATH` to include `/nix/var/nix/profiles/default/bin` - if (isTRUE(is_rstudio_session()) && isFALSE(is_nix_rsession())) { - set_nix_path() - } - - has_nix_shell <- nix_shell_available() # TRUE if yes, FALSE if no - stopifnot("`nix-shell` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = - isTRUE(has_nix_shell)) - - if (isFALSE(has_nix_shell)) { - stop( - paste0("`nix-shell` is needed but is not available in your current ", - "shell environment.\n", - "* If you are in an R session of your host operating system, you - either\n1a) need to install Nix first, or if you have already done so ", - "\n", - "1b) make sure that the location of the nix store is in the `PATH` - variable of this R session (mostly necessary in RStudio).\n", - "* If you ran `with_nix()` from R launched in a `nix-shell`, you need - to make sure that `pkgs.nix` is in the `buildInput` for ", - "`pkgs.mkShell`.\nIf you used `rix::rix()` to generate your main nix - configuration of this session, just regenerate it with the additonal - argument `system_pkgs = 'nix'."), - call. = FALSE - ) - } - - program <- match.arg(program) - exec_mode <- match.arg(exec_mode) - message_type <- match.arg(message_type) - - if (program == "R") { - - # get the function arguments as a pairlist; - # save formal arguments of pairlist via `tag = value`; e.g., if we have a - # `expr = function(p = p_root) dir(path = p)`, the input object - # to be serialized will be serialized under `"p.Rds"` in a tmp dir, and - # will contain object `p_root`, which is defined in the global environment - # and bound to `"."` (project root) - args <- as.list(formals(expr)) - - cat("\n### Prepare to exchange arguments and globals for `expr`", - "between the host and Nix R sessions ###\n") - - # 1) save all function args onto a temporary folder each with - # `` and `value` as serialized objects from RAM --------------------- - temp_dir <- tempdir() - serialize_args(args, temp_dir) - - # cast list of symbols/names and calls to list of strings; this is to prepare - # deparsed version (string) of deserializing arguments from disk; - # elements of args for now should be of type "symbol" or "language" - args_vec <- vapply(args, deparse, FUN.VALUE = character(1L)) - - # todo in `rnix_deparsed`: - # => locate all global variables used by function - # https://github.com/cran/codetools/blob/master/R/codetools.R - # http://adv-r.had.co.nz/Expressions.html#ast-funs - - # code inspection: generates messages with potential problems - check_expr(expr) - - globals_expr <- recurse_find_check_globals(expr, args_vec) - - # wrapper around `serialize_lobjs()` - globals <- serialize_globals(globals_expr, temp_dir) - - # extract additional packages to export - pkgs <- serialize_pkgs(globals_expr, temp_dir) - - # 2) deserialize formal arguments of `expr` in nix session - # and necessary global objects --------------------------------------------- - # 3) serialize resulting output from evaluating function given as `expr` - - # main code to be run in nix R session - rnix_file <- file.path(temp_dir, "with_nix_r.R") - - rnix_quoted <- quote_rnix( - expr, program, message_type, args_vec, globals, pkgs, temp_dir, rnix_file - ) - rnix_deparsed <- deparse_chr1(expr = rnix_quoted, collapse = "\n") - - # 4): for 2) and 3) write script to disk, to run later via `Rscript` from - # `nix-shell` - # environment - r_version_file <- file.path(temp_dir, "nix-r-version.txt") - writeLines(text = rnix_deparsed, file(rnix_file)) - - # 3) run expression in nix session, based on temporary script - cat(paste0("==> Running deparsed expression via `nix-shell`", " in ", - exec_mode, " mode:\n\n"#, - # paste0(rnix_deparsed, collapse = " ") - )) - - # command to run deparsed R expression via nix-shell - cmd_rnix_deparsed <- c( - file.path(project_path, "default.nix"), - "--pure", # required for to have nix glibc - "--run", - sprintf( - "Rscript --vanilla '%s'", - rnix_file - ) - ) - - proc <- switch(exec_mode, - "blocking" = sys::exec_internal(cmd = "nix-shell", cmd_rnix_deparsed), - "non-blocking" = sys::exec_background( - cmd = "nix-shell", cmd_rnix_deparsed), - stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') - ) - if (exec_mode == "non-blocking") { - poll_sys_proc_nonblocking(cmd = cmd_rnix_deparsed, proc, what = "expr") - } else if (exec_mode == "blocking") { - poll_sys_proc_blocking(cmd = cmd_rnix_deparsed, proc, what = "expr") - } - } else if (program == "shell") { # end of `if (program == "R")` - shell_cmd <- c( - file.path(project_path, "default.nix"), - "--pure", - "--run", - expr - ) - proc <- switch(exec_mode, - "blocking" = sys::exec_internal(cmd = "nix-shell", shell_cmd), - "non-blocking" = sys::exec_background( - cmd = "nix-shell", shell_cmd), - stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') - ) - } - - # 5) deserialize final output of `expr` evaluated in nix-shell - # into host R session - if (program == "R") { - out <- readRDS(file = file.path(temp_dir, "_out.Rds")) - on.exit(close(file(rnix_file))) - } else if (program == "shell") { - if (exec_mode == "non-blocking") { - status <- poll_sys_proc_nonblocking( - cmd = shell_cmd, proc, what = "expr" - ) - out <- status - } else if (exec_mode == "blocking") { - poll_sys_proc_blocking(cmd = shell_cmd, proc, what = "expr") - out <- proc - out$stdout <- sys::as_text(out$stdout) - out$stderr <- sys::as_text(out$stderr) - } - } - - cat("\n### Finished code evaluation in `nix-shell` ###\n") - - # return output from evaluated function - cat("\n* Evaluating `expr` in `nix-shell` returns:\n") - if (program == "R") { - print(out) - } else if (program == "shell") { - print(out$stdout) - } - - cat("") - return(out) -} - - -#' serialize language objects -#' @noRd -serialize_lobjs <- function(lobjs, temp_dir) { - invisible({ - for (i in seq_along(lobjs)) { - if (!any(nzchar(deparse(lobjs[[i]])))) { - # for unnamed arguments like `expr = function(x) print(x)` - # x would be an empty symbol, see also ; i.e. arguments without - # default expressions; i.e. tagged arguments with no value - # https://stackoverflow.com/questions/3892580/create-missing-objects-aka-empty-symbols-empty-objects-needed-for-f - lobjs[[i]] <- as.symbol(names(lobjs)[i]) - } - saveRDS( - object = lobjs[[i]], - file = file.path(temp_dir, paste0(names(lobjs)[i], ".Rds")) - ) - } - }) -} - -serialize_args <- function(args, temp_dir) { - invisible({ - for (i in seq_along(args)) { - if (!nzchar(deparse(args[[i]]))) { - # for unnamed arguments like `expr = function(x) print(x)` - # x would be an empty symbol, see also ; i.e. arguments without - # default expressions; i.e., tagged arguments with no value - # https://stackoverflow.com/questions/3892580/create-missing-objects-aka-empty-symbols-empty-objects-needed-for-f - args[[i]] <- as.symbol(names(args)[i]) - } - args[[i]] <- get(as.character(args[[i]])) - saveRDS( - object = args[[i]], - file = file.path(temp_dir, paste0(names(args)[i], ".Rds")) - ) - } - }) -} - - -#' @noRd -check_expr <- function(expr) { - cat("* checking code in `expr` for potential problems:\n", - "`codetools::checkUsage(fun = expr)`\n") - codetools::checkUsage(fun = expr) - cat("\n") - } - - -#' @noRd -# to determine which extra packages to load in Nix R prior evaluating `expr` -get_expr_extra_pkgs <- function(globals_expr) { - envs_check <- lapply(globals_expr, where) - names_envs_check <- vapply(envs_check, environmentName, character(1L)) - - default_pkgnames <- paste0("package:", getOption("defaultPackages")) - pkgenvs_attached <- setdiff( - grep("^package:", names_envs_check, value = TRUE), - c(default_pkgnames, "base") - ) - if (!length(pkgenvs_attached) == 0L) { - pkgs_to_attach <- gsub("^package:", "", pkgenvs_attached) - return(pkgs_to_attach) - } else { - return(NULL) - } -} - - -#' @noRd -is_empty <- function(x) identical(x, emptyenv()) - - -#' @noRd -where <- function(name, env = parent.frame()) { - while(!is_empty(env)) { - if (exists(name, envir = env, inherits = FALSE)) { - return(env) - } - # inspect parent - env <- parent.env(env) - } -} - -#' Finds and checks global functions and variables recursively for closure -#' `expr` -#' @noRd -recurse_find_check_globals <- function(expr, args_vec) { - - cat("* checking code in `expr` for potential problems:\n") - codetools::checkUsage(fun = expr) - cat("\n") - - globals_expr <- codetools::findGlobals(fun = expr) - globals_lst <- classify_globals(globals_expr, args_vec) - - round_i <- 1L - - repeat { - - get_globals_exprs <- function(globals_lst) { - globals_exprs <- names(unlist(Filter(function(x) !is.null(x), - unname(globals_lst[c("globalenv_fun", "env_fun")])))) - return(globals_exprs) - } - - if (round_i == 1L) { - # first round - globals_exprs <- get_globals_exprs(globals_lst) - } else { - # successive rounds - globals_exprs <- unlist(lapply(globals_lst, get_globals_exprs)) - } - - cat("* checking code in `globals_exprs` for potential problems:\n") - lapply( - globals_exprs, - codetools::checkUsage - ) - cat("\n") - - globals_new <- lapply( - globals_exprs, - function(x) codetools::findGlobals(fun = x) - ) - - globals_lst_new <- lapply( - globals_new, - function(x) classify_globals(globals_expr = x, args_vec) - ) - - if (round_i == 1L) { - result_list <- c(list(globals_lst), globals_lst_new) - } else { - result_list <- c(result_list, globals_lst_new) - } - - # prepare current globals to find new globals one recursion level deeper - # in the call stack in the next repeat - globals_lst <- globals_lst_new - - globals_lst <- lapply(globals_lst, function(x) lapply(x, unlist)) - - # packages need to be excluded for getting more globals - globals_lst <- lapply( - globals_lst, - function(x) { - x[c("globalenv_fun", "globalenv_other", "env_other", "env_fun")] - } - ) - - globals_null <- all(is.null(unlist(globals_lst))) - # TRUE if no more candidate global values - all_non_pkgs_null <- all(globals_null) - - round_i <- round_i + 1L - - if (is.null(globals_lst) || all_non_pkgs_null) break - } - - result_list <- Filter(function(x) !is.null(x), result_list) - result_list <- lapply( - result_list, - function(x) Filter(function(x) !is.null(x), x) - ) - - pkgs <- unlist(lapply(result_list, "[", "pkgs")) - - unlist_unname <- function(x) { - unlist( - lapply(x, function(x) unlist(unname(x))) - ) - } - - globalenv_fun <- lapply(result_list, "[", "globalenv_fun") - globalenv_fun <- unlist_unname(globalenv_fun) - - globalenv_other <- lapply(result_list, "[", "globalenv_other") - globalenv_other <- unlist_unname(globalenv_other) - - env_other <- lapply(result_list, "[", "env_other") - env_other <- unlist_unname(env_other) - - env_fun = lapply(result_list, "[", "env_fun") - env_fun <- unlist_unname(env_fun) - - exports <- list( - pkgs = pkgs, - globalenv_fun = globalenv_fun, - globalenv_other = globalenv_other, - env_other = env_other, - env_fun = env_fun - ) - - return(exports) -} - -#' @noRd -classify_globals <- function(globals_expr, args_vec) { - envs_check <- lapply(globals_expr, where) - names(envs_check) <- globals_expr - - vec_envs_check <- vapply(envs_check, environmentName, character(1L)) - # directly remove formals - vec_envs_check <- vec_envs_check[!names(vec_envs_check) %in% args_vec] - if (length(vec_envs_check) == 0L) { - vec_envs_check <- NULL - } - - if (!is.null(vec_envs_check)) { - globs_pkg <- grep("^package:", vec_envs_check, value = TRUE) - if (length(globs_pkg) == 0L) { - globs_pkg <- NULL - } - # globs base can be ignored - globs_base <- grep("^base$", vec_envs_check, value = TRUE) - globs_globalenv <- grep("^R_GlobalEnv$", vec_envs_check, value = TRUE) - globs_globalenv <- Filter(nzchar, globs_globalenv) - # empty globs; can be ignored for now - globs_empty <- Filter(function(x) !nzchar(x), vec_envs_check) - if (length(globs_empty) == 0L) { - globs_empty <- NULL - } - globs_other <- vec_envs_check[!names(vec_envs_check) %in% - names(c(globs_pkg, globs_globalenv, globs_empty, globs_base))] - if (length(globs_other) == 0L) { - globs_other <- NULL - } - } - - is_globalenv_funs <- vapply( - names(globs_globalenv), function(x) is.function(get(x)), - FUN.VALUE = logical(1L) - ) - - is_otherenv_funs <- vapply( - names(globs_other), function(x) is.function(get(x)), - FUN.VALUE = logical(1L) - ) - - globs_globalenv_fun <- globs_globalenv[is_globalenv_funs] - if (length(globs_globalenv_fun) == 0L) { - globs_globalenv_fun <- NULL - } - globs_globalenv_other <- globs_globalenv[!is_globalenv_funs] - if (length(globs_globalenv_other) == 0L) { - globs_globalenv_other <- NULL - } - - globs_otherenv_fun <- globs_other[is_otherenv_funs] - if (length(globs_otherenv_fun) == 0L) { - globs_otherenv_fun <- NULL - } - globs_otherenv_other <- globs_other[!is_otherenv_funs] - if (length(globs_otherenv_other) == 0L) { - globs_otherenv_other <- NULL - } - - default_pkgnames <- paste0("package:", getOption("defaultPackages")) - pkgenvs_attached <- setdiff(globs_pkg, c(default_pkgnames, "base")) - - if (!length(pkgenvs_attached) == 0L) { - pkgs_to_attach <- gsub("^package:", "", pkgenvs_attached) - } else { - pkgs_to_attach <- NULL - } - - globs_classified <- list( - globalenv_fun = globs_globalenv_fun, - globalenv_other = globs_globalenv_other, - env_other = globs_otherenv_other, - env_fun = globs_otherenv_fun, - pkgs = pkgs_to_attach - ) - globs_null <- all(vapply(globs_classified, is.null, logical(1L))) - if (globs_null) globs_classified <- NULL - - return(globs_classified) -} - - -# wrapper to serialize expressions of all global objects found -#' @noRd -serialize_globals <- function(globals_expr, temp_dir) { - funs <- globals_expr$globalenv_fun - if (!is.null(funs)) { - cat("=> Saving global functions to disk:", paste(names(funs)), "\n") - globalenv_funs <- lapply( - names(funs), - function(x) get(x = x, envir = .GlobalEnv) - ) - names(globalenv_funs) <- names(globals_expr$globalenv_fun) - serialize_lobjs(lobjs = globalenv_funs, temp_dir) - } - others <- globals_expr$globalenv_other - if (!is.null(others)) { - cat("=> Saving non-function object(s), e.g. other environments:", - paste(names(others)), "\n" - ) - globalenv_others <- lapply( - names(others), - function(x) get(x = x, envir = .GlobalEnv) - ) - names(globalenv_others) <- names(globals_expr$globalenv_other) - serialize_lobjs(lobjs = globalenv_others, temp_dir) - } - env_funs <- globals_expr$env_fun - if (!is.null(env_funs)) { - cat("=> Serializing function(s) from other environment(s):", - paste(names(env_funs)), "\n") - env_funs <- lapply( - names(env_funs), - function(x) get(x = x) - ) - names(env_funs) <- names(globals_expr$env_fun) - serialize_lobjs(lobjs = env_funs, temp_dir) - } - env_others <- globals_expr$env_other - if (!is.null(env_others)) { - cat("=> Serializing non-function object(s) from custom environment(s)::", - paste(names(env_others)), "\n" - ) - env_others <- lapply( - names(env_others), - function(x) get(x = x) - ) - names(env_others) <- names(globals_expr$env_other) - serialize_lobjs(lobjs = env_others, temp_dir) - } - - return(c(funs, others, env_funs, env_others)) -} - - -#' @noRd -serialize_pkgs <- function(globals_expr, temp_dir) { - pkgs <- globals_expr$pkgs - if (!is.null(pkgs)) { - cat("=> Serializing package(s) required to run `expr`:\n", - paste(pkgs), "\n" - ) - } - saveRDS( - object = pkgs, - file = file.path(temp_dir, "_pkgs.Rds") - ) - return(pkgs) -} - -# build deparsed script via language objects; -# reads like R code, and avoids code injection -quote_rnix <- function(expr, - program, - message_type, - args_vec, - globals, - pkgs, - temp_dir, - rnix_file) { - expr_quoted <- bquote( { - cat("### Start evaluating `expr` in `nix-shell` ###") - cat("\n* wrote R script evaluated via `Rscript` in `nix-shell`:", - .(rnix_file)) - temp_dir <- .(temp_dir) - cat("\n", Sys.getenv("NIX_PATH")) - # fix library paths for nix R on macOS and linux; avoid permission issue - current_paths <- .libPaths() - userlib_paths <- Sys.getenv("R_LIBS_USER") - user_dir <- grep(paste(userlib_paths, collapse = "|"), current_paths) - new_paths <- current_paths[-user_dir] - .libPaths(new_paths) - r_version_num <- paste0(R.version$major, ".", R.version$minor) - cat("\n* using Nix with R version", r_version_num, "\n\n") - # assign `args_vec` as in c(...) form. - args_vec <- .(with_assign_vecnames_call(vec = args_vec)) - # deserialize arguments from disk - for (i in seq_along(args_vec)) { - nm <- args_vec[i] - obj <- args_vec[i] - assign( - x = nm, - value = readRDS(file = file.path(temp_dir, paste0(obj, ".Rds"))) - ) - cat( - paste0(" => reading file ", "'", obj, ".Rds", "'", - " for argument named `", obj, "`\n") - ) - } - - globals <- .(with_assign_vecnames_call(vec = globals)) - for (i in seq_along(globals)) { - nm <- globals[i] - obj <- globals[i] - assign( - x = nm, - value = readRDS(file = file.path(temp_dir, paste0(obj, ".Rds"))) - ) - cat( - paste0(" => reading file ", "'", obj, ".Rds", "'", - " for global object named `", obj, "`\n") - ) - } - - # for now name of character vector containing packages is hard-coded - # pkgs <- .(with_assign_vecnames_call(vec = pkgs)) - # pkgs <- .(pkgs) - pkgs <- .(with_assign_vec_call(vec = pkgs)) - lapply(pkgs, library, character.only = TRUE) - - # execute function call in `expr` with list of correct args - lst <- as.list(args_vec) - names(lst) <- args_vec - lst <- lapply(lst, as.name) - rnix_out <- do.call(.(expr), lst) - cat("\n* called `expr` with args", args_vec, ":\n") - message_type <- .(message_type) - if (message_type == "verbose") { - # cat("\n", deparse(.(expr))) # not nicely formatted, use print - # print(.(expr)) - } - cat("\n* The type of the output object returned by `expr` is", - typeof(rnix_out)) - saveRDS(object = rnix_out, file = file.path(temp_dir, "_out.Rds")) - cat("\n* Saved output to", file.path(temp_dir, "_out.Rds")) - cat("\n\n* the following objects are in the global environment:\n") - cat(ls()) - cat("\n") - cat("\n* `sessionInfo()` output:\n") - cat(capture.output(sessionInfo()), sep = "\n") - } ) # end of `bquote()` - - return(expr_quoted) -} - -# https://github.com/cran/codetools/blob/master/R/codetools.R -# finding global variables - -# reconstruct argument vector (character) in Nix R; -# build call to generate `args_vec` -#' @noRd -with_assign_vecnames_call <- function(vec) { - cl <- call("c") - for (i in seq_along(vec)) { - cl[[i + 1L]] <- names(vec[i]) - } - return(cl) -} - -#' @noRd -with_assign_vec_call <- function(vec) { - cl <- call("c") - for (i in seq_along(vec)) { - cl[[i + 1L]] <- vec[i] - } - return(cl) -} - -# this is what `deparse1()` does, however, it is only since 4.0.0 -#' @noRd -deparse_chr1 <- function(expr, width.cutoff = 500L, collapse = " ", ...) { - paste(deparse(expr, width.cutoff, ...), collapse = collapse) -} - -#' @noRd -with_expr_deparse <- function(expr) { - sprintf( - 'run_expr <- %s\n', - deparse_chr1(expr = expr, collapse = "\n") - ) -} - -#' @noRd -nix_shell_available <- function() { - which_nix_shell <- Sys.which("nix-shell") - if (nzchar(which_nix_shell)) { - return(TRUE) - } else { - return(FALSE) - } -} - -#' @noRd -create_shell_nix <- function(path = file.path("inst", "extdata", - "with_nix", "default.nix")) { - if (!dir.exists(dirname(path))) { - dir.create(dirname(path), recursive = TRUE) - } - - rix( - r_ver = "latest", - r_pkgs = NULL, - system_pkgs = NULL, - git_pkgs = NULL, - ide = "other", - project_path = dirname(path), - overwrite = TRUE, - shell_hook = NULL - ) -} diff --git a/R/get_latest.R b/R/get_latest.R new file mode 100644 index 00000000..211f4025 --- /dev/null +++ b/R/get_latest.R @@ -0,0 +1,22 @@ +# WARNING - Generated by {fusen} from dev/flat_get_latest.Rmd: do not edit by hand + +#' get_latest Get the latest R version and packages +#' @return A character. The commit hash of the latest nixpkgs-unstable revision +#' @importFrom httr content GET stop_for_status +#' @importFrom jsonlite fromJSON +#' +#' @noRd +get_latest <- function() { + api_url <- "https://api.github.com/repos/NixOS/nixpkgs/commits?sha=nixpkgs-unstable" + + tryCatch({ + response <- httr::GET(url = api_url) + httr::stop_for_status(response) + commit_data <- jsonlite::fromJSON(httr::content(response, "text")) + latest_commit <- commit_data$sha[1] + return(latest_commit) + }, error = function(e) { + cat("Error:", e$message, "\n") + return(NULL) + }) +} diff --git a/R/get_sri_hash_deps.R b/R/get_sri_hash_deps.R new file mode 100644 index 00000000..fd20016a --- /dev/null +++ b/R/get_sri_hash_deps.R @@ -0,0 +1,25 @@ +# WARNING - Generated by {fusen} from dev/flat_get_sri_hash_deps.Rmd: do not edit by hand + +#' get_sri_hash_deps Get the SRI hash of the NAR serialization of a Github repo +#' @param repo_url A character. The URL to the package's Github repository or to the `.tar.gz` package hosted on CRAN. +#' @param branch_name A character. The branch of interest, NULL for archived CRAN packages. +#' @param commit A character. The commit hash of interest, for reproducibility's sake, NULL for archived CRAN packages. +#' @importFrom httr content GET http_error +#' @return The SRI hash as a character +#' @noRd +get_sri_hash_deps <- function(repo_url, branch_name, commit){ + result <- httr::GET(paste0("http://git2nixsha.dev:1506/hash?repo_url=", + repo_url, + "&branchName=", + branch_name, + "&commit=", + commit)) + + if(http_error(result)){ + stop(paste0("Error in pulling URL: ", repo_url, ". If it's a Github repo, check the url, branch name and commit. Are these correct? If it's an archived CRAN package, check the name of the package and the version number.")) + } + + + lapply(httr::content(result), unlist) + +} diff --git a/R/nix_build.R b/R/nix_build.R new file mode 100644 index 00000000..6e305e25 --- /dev/null +++ b/R/nix_build.R @@ -0,0 +1,130 @@ +# WARNING - Generated by {fusen} from dev/flat_nix_build.Rmd: do not edit by hand + +#' Invoke shell command `nix-build` from an R session +#' @param project_path Path to the folder where the `default.nix` file resides. +#' The default is `"."`, which is the working directory in the current R +#' session. +#' @param exec_mode Either `"blocking"` (default) or `"non-blocking`. This +#' will either block the R session while the `nix-build` shell command is +#' executed, or run `nix-build` in the background ("non-blocking"). +#' @return integer of the process ID (PID) of `nix-build` shell command +#' launched, if `nix_build()` call is assigned to an R object. Otherwise, it +#' will be returned invisibly. +#' @details The `nix-build` command line interface has more arguments. We will +#' probably not support all of them in this R wrapper, but currently we have +#' support for the following `nix-build` flags: +#' - `--max-jobs`: Maximum number of build jobs done in parallel by Nix. +#' According to the official docs of Nix, it defaults to `1`, which is one +#' core. This option can be useful for shared memory multiprocessing or +#' systems with high I/O latency. To set `--max-jobs` used, you can declare +#' with `options(rix.nix_build_max_jobs = )`. Once you call +#' `nix_build()` the flag will be propagated to the call of `nix-build`. +#' @export +#' @examples +#' \dontrun{ +#' nix_build() +#' } +nix_build <- function(project_path = ".", + exec_mode = c("blocking", "non-blocking")) { + has_nix_build <- nix_build_installed() # TRUE if yes, FALSE if no + nix_file <- file.path(project_path, "default.nix") + + stopifnot( + "`project_path` must be character of length 1." = + is.character(project_path) && length(project_path) == 1L, + "`project_path` has no `default.nix` file. Use one that contains `default.nix`" = + file.exists(nix_file), + "`nix-build` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = + isTRUE(has_nix_build) + ) + exec_mode <- match.arg(exec_mode) + + max_jobs <- getOption("rix.nix_build_max_jobs", default = 1L) + stopifnot("option `rix.nix_build_max_jobs` is not integerish" = + is_integerish(max_jobs)) + max_jobs <- as.integer(max_jobs) + + if (max_jobs == 1L) { + cmd <- c("nix-build", nix_file) + } else { + cmd <- c("nix-build", "--max-jobs", as.character(max_jobs), nix_file) + } + + cat(paste0("Launching `", paste0(cmd, collapse = " "), "`", " in ", + exec_mode, " mode\n")) + + proc <- switch(exec_mode, + "blocking" = sys::exec_internal(cmd = cmd), + "non-blocking" = sys::exec_background(cmd = cmd), + stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') + ) + + if (exec_mode == "non-blocking") { + poll_sys_proc_nonblocking(cmd, proc, what = "nix-build") + } else if (exec_mode == "blocking") { + poll_sys_proc_blocking(cmd, proc, what = "nix-build") + } + + # todo (?): clean zombies for background/non-blocking mode + + return(invisible(proc)) +} + +#' @noRd +poll_sys_proc_blocking <- function(cmd, proc, + what = c("nix-build", "expr")) { + what <- match.arg(what) + status <- proc$status + if (status == 0L) { + cat(paste0("\n==> ", sys::as_text(proc$stdout))) + cat(paste0("\n==> `", what, "` succeeded!", "\n")) + } else { + msg <- nix_build_exit_msg() + cat(paste0("`", cmd, "`", " failed with ", msg)) + } + return(invisible(status)) +} + +#' @noRd +poll_sys_proc_nonblocking <- function(cmd, proc, + what = c("nix-build", "expr")) { + what <- match.arg(what) + cat(paste0("\n==> Process ID (PID) is ", proc, ".")) + cat("\n==> Receiving stdout and stderr streams...\n") + status <- sys::exec_status(proc, wait = TRUE) + if (status == 0L) { + cat(paste0("\n==> `", what, "` succeeded!")) + } + return(invisible(status)) +} + +#' @noRd +is_integerish <- function(x, tol = .Machine$double.eps^0.5) { + return(abs(x - round(x)) < tol) +} + +#' @noRd +nix_build_installed <- function() { + exit_code <- system2("command", "-v", "nix-build") + if (exit_code == 0L) { + return(invisible(TRUE)) + } else { + return(invisible(FALSE)) + } +} + +#' @noRd +nix_build_exit_msg <- function(x) { + x_char <- as.character(x) + + err_msg <- switch( + x_char, + "100" = "generic build failure (100).", + "101" = "build timeout (101).", + "102" = "hash mismatch (102).", + "104" = "not deterministic (104).", + stop(paste0("general exit code ", x_char, ".")) + ) + + return(err_msg) +} diff --git a/R/rix.R b/R/rix.R new file mode 100644 index 00000000..79c5d97a --- /dev/null +++ b/R/rix.R @@ -0,0 +1,372 @@ +# WARNING - Generated by {fusen} from dev/flat_rix.Rmd: do not edit by hand + +#' rix Generates a Nix expression that builds a reproducible development environment +#' @return Nothing, this function only has the side-effect of writing a file +#' called "default.nix" in the working directory. This file contains the +#' expression to build a reproducible environment using the Nix package +#' manager. +#' @param r_ver Character, defaults to "latest". The required R version, for example "4.0.0". +#' To use the latest version of R, use "latest", if you need the latest, bleeding edge version +#' of R and packages, then use "latest". You can check which R versions are available using `available_r`. +#' For reproducibility purposes, you can also provide a nixpkgs revision. +#' @param r_pkgs Vector of characters. List the required R packages for your +#' analysis here. +#' @param system_pkgs Vector of characters. List further software you wish to install that +#' are not R packages such as command line applications for example. +#' @param git_pkgs List. A list of packages to install from Git. See details for more information. +#' @param tex_pkgs Vector of characters. A set of tex packages to install. Use this if you need to compile `.tex` documents, or build PDF documents using Quarto. If you don't know which package to add, start by adding "amsmath". See the Vignette "Authoring LaTeX documents" for more details. +#' @param ide Character, defaults to "other". If you wish to use RStudio to work +#' interactively use "rstudio" or "code" for Visual Studio Code. For other editors, +#' use "other". This has been tested with RStudio, VS Code and Emacs. If other +#' editors don't work, please open an issue. +#' @param project_path Character, defaults to the current working directory. Where to write +#' `default.nix`, for example "/home/path/to/project". +#' The file will thus be written to the file "/home/path/to/project/default.nix". +#' @param overwrite Logical, defaults to FALSE. If TRUE, overwrite the `default.nix` +#' file in the specified path. +#' @param print Logical, defaults to FALSE. If TRUE, print `default.nix` to console. +#' @param shell_hook Character, defaults to `"R --vanilla"`. Commands added to the shell_hook get +#' executed when the Nix shell starts (via `shellHook`). So by default, using `nix-shell default.nix` will +#' start R. Set to NULL if you want bash to be started instead. +#' @details This function will write a `default.nix` in the chosen path. Using +#' the Nix package manager, it is then possible to build a reproducible +#' development environment using the `nix-build` command in the path. This +#' environment will contain the chosen version of R and packages, and will not +#' interfere with any other installed version (via Nix or not) on your +#' machine. Every dependency, including both R package dependencies but also +#' system dependencies like compilers will get installed as well in that +#' environment. If you use RStudio for interactive work, then set the +#' `rstudio` parameter to `TRUE`. If you use another IDE (for example Emacs or +#' Visual Studio Code), you do not need to add it to the `default.nix` file, +#' you can simply use the version that is installed on your computer. Once you built +#' the environment using `nix-build`, you can drop into an interactive session +#' using `nix-shell`. See the "Building reproducible development environments with rix" +#' vignette for detailled instructions. +#' Packages to install from Github must be provided in a list of 4 elements: +#' "package_name", "repo_url", "branch_name" and "commit". +#' This argument can also be a list of lists of these 4 elements. It is also possible to install old versions +#' of packages by specifying a version. For example, to install the latest +#' version of `{AER}` but an old version of `{ggplot2}`, you could +#' write: `r_pkgs = c("AER", "ggplot2@2.2.1")`. Note +#' however that doing this could result in dependency hell, because an older +#' version of a package might need older versions of its dependencies, but other +#' packages might need more recent versions of the same dependencies. If instead you +#' want to use an environment as it would have looked at the time of `{ggplot2}`'s +#' version 2.2.1 release, then use the Nix revision closest to that date, by setting +#' `r_ver = "3.1.0"`, which was the version of R current at the time. This +#' ensures that Nix builds a completely coherent environment. +#' By default, the nix shell will be configured with `"en_US.UTF-8"` for the +#' relevant locale variables (`LANG`, `LC_ALL`, `LC_TIME`, `LC_MONETARY`, +#' `LC_PAPER`, `LC_MEASUREMENT`). This is done to ensure locale +#' reproducibility by default in Nix environments created with `rix()`. +#' If there are good reasons to not stick to the default, you can set your +#' preferred locale variables via +#' `options(rix.nix_locale_variables = list(LANG = "de_CH.UTF-8", <...>)` +#' and the aforementioned locale variable names. +#' @export +#' @examples +#' \dontrun{ +#' # Build an environment with the latest version of R +#' # and the dplyr and ggplot2 packages +#' rix(r_ver = "latest", +#' r_pkgs = c("dplyr", "ggplot2"), +#' system_pkgs = NULL, +#' git_pkgs = NULL, +#' ide = "code", +#' project_path = path_default_nix, +#' overwrite = TRUE, +#' print = TRUE) +#' } +rix <- function(r_ver = "latest", + r_pkgs = NULL, + system_pkgs = NULL, + git_pkgs = NULL, + tex_pkgs = NULL, + ide = "other", + project_path = ".", + overwrite = FALSE, + print = FALSE, + shell_hook = "R --vanilla"){ + + stopifnot("'ide' has to be one of 'other', 'rstudio' or 'code'" = (ide %in% c("other", "rstudio", "code"))) + + project_path <- if(project_path == "."){ + "default.nix" + } else { + paste0(project_path, "/default.nix") + } + + # Generate the correct text for the header depending on wether + # an R version or a Nix revision is supplied to `r_ver` + if(nchar(r_ver) > 20){ + r_ver_text <- paste0("as it was as of nixpkgs revision: ", r_ver) + } else { + r_ver_text <- paste0("version ", r_ver) + } + + # Find the Nix revision to use + nix_revision <- find_rev(r_ver) + + project_path <- file.path(project_path) + + rix_call <- match.call() + + generate_rix_call <- function(rix_call, nix_revision){ + + rix_call$r_ver <- nix_revision + + rix_call <- paste0("# >", deparse1(rix_call)) + + gsub(",", ",\n# >", rix_call) + } + + # Get the rix version + rix_version <- utils::packageVersion("rix") + + generate_header <- function(rix_version, + nix_revision, + r_ver_text, + rix_call){ + + if(identical(Sys.getenv("TESTTHAT"), "true")){ + sprintf(' +let + pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/%s.tar.gz") {}; +', +nix_revision) + } else { + sprintf('# This file was generated by the {rix} R package v%s on %s +# with following call: +%s +# It uses nixpkgs\' revision %s for reproducibility purposes +# which will install R %s +# Report any issues to https://github.com/b-rodrigues/rix +let + pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/%s.tar.gz") {}; +', +rix_version, +Sys.Date(), +generate_rix_call(rix_call, nix_revision), +nix_revision, +r_ver_text, +nix_revision +) + } + + } + + # Now we need to generate all the different sets of packages + # to install. Let's start by the CRAN packages, current + # and archived. The function below builds the strings. + get_rPackages <- function(r_pkgs){ + + # in case users pass something like c("dplyr", "tidyr@1.0.0") + # r_pkgs will be "dplyr" only + # and "tidyr@1.0.0" needs to be handled by fetchzips + r_and_archive_pkgs <- detect_versions(r_pkgs) + + # overwrite r_pkgs + r_pkgs <- r_and_archive_pkgs$cran_packages + + # get archive_pkgs + archive_pkgs <- r_and_archive_pkgs$archive_packages + + r_pkgs <- if(ide == "code"){ + c(r_pkgs, "languageserver") + } else { + r_pkgs + } + + rPackages <- paste(r_pkgs, collapse = ' ') + + rPackages <- gsub('\\.', '_', rPackages) + + list("rPackages" = rPackages, + "archive_pkgs" = archive_pkgs) + + } + + # Get the two lists. One list is current CRAN packages + # the other is archived CRAN packages. + cran_pkgs <- get_rPackages(r_pkgs) + + # we need to know if the user wants R packages + + flag_rpkgs <- if(is.null(cran_pkgs$rPackages) | cran_pkgs$rPackages == ""){ + "" + } else { + "rpkgs" + } + + # generate_* function generate the actual Nix code + generate_rpkgs <- function(rPackages) { + if (flag_rpkgs == ""){ + NULL + } else { + sprintf('rpkgs = builtins.attrValues { + inherit (pkgs.rPackages) %s; +}; +', +rPackages) + } + } + + # Texlive packages + generate_tex_pkgs <- function(tex_pkgs) { + if (!is.null(tex_pkgs)) { + + tex_pkgs <- paste(tex_pkgs, collapse = ' ') + + sprintf('tex = (pkgs.texlive.combine { + inherit (pkgs.texlive) scheme-small %s; +}); +', +tex_pkgs) + } + } + + flag_tex_pkgs <- if(is.null(tex_pkgs)){ + "" + } else { + "tex" + } + + # system packages + get_system_pkgs <- function(system_pkgs, r_pkgs){ + + system_pkgs <- if(any(grepl("quarto", r_pkgs))){ + unique(c(system_pkgs, "quarto")) + } else { + system_pkgs + } + + paste(system_pkgs, collapse = ' ') + } + + flag_git_archive <- if(!is.null(cran_pkgs$archive) | !is.null(git_pkgs)){ + "git_archive_pkgs" + } else { + "" + } + + generate_git_archived_packages <- function(git_pkgs, archive_pkgs){ + if(flag_git_archive == ""){ + NULL + } else { + sprintf('git_archive_pkgs = [%s];\n', + fetchpkgs(git_pkgs, archive_pkgs) + ) + } + } + + + # `R` needs to be added. If we were using the rWrapper + # this wouldn't be needed, but we're not so we need + # to add it. + generate_system_pkgs <- function(system_pkgs, r_pkgs){ + sprintf('system_packages = builtins.attrValues { + inherit (pkgs) R glibcLocales nix %s; +}; +', +get_system_pkgs(system_pkgs, r_pkgs)) + } + + generate_locale_variables <- function() { + locale_defaults <- list( + LANG = "en_US.UTF-8", + LC_ALL = "en_US.UTF-8", + LC_TIME = "en_US.UTF-8", + LC_MONETARY = "en_US.UTF-8", + LC_PAPER = "en_US.UTF-8", + LC_MEASUREMENT = "en_US.UTF-8" + ) + locale_variables <- getOption( + "rix.nix_locale_variables", + default = locale_defaults + ) + valid_vars <- all(names(locale_variables) %in% names(locale_defaults)) + if (!isTRUE(valid_vars)) { + stop("`options(rix.nix_locale_variables = list())` ", + "only allows the following element names (locale variables):\n", + paste(names(locale_defaults), collapse = "; "), + call. = FALSE) + } + locale_vars <- paste( + Map(function(x, nm) paste0(nm, ' = ', '"', x, '"'), + nm = names(locale_variables), x = locale_variables), + collapse = ";\n " + ) + paste0(locale_vars, ";\n") + } + + generate_rstudio_pkgs <- function(ide, flag_git_archive, flag_rpkgs){ + if(ide == "rstudio"){ + sprintf('rstudio_pkgs = pkgs.rstudioWrapper.override { + packages = [ %s %s ]; +}; +', +flag_git_archive, +flag_rpkgs +) + } else { + NULL + } + } + + flag_rstudio <- if (ide == "rstudio") "rstudio_pkgs" else "" + + shell_hook <- if (!is.null(shell_hook) && nzchar(shell_hook)) { + paste0('shellHook = "', shell_hook, '";') + } else {''} + + # Generate the shell + generate_shell <- function(flag_git_archive, + flag_rpkgs){ + sprintf('in + pkgs.mkShell { + %s + %s + buildInputs = [ %s %s %s system_packages %s ]; + %s + }', + generate_locale_archive(detect_os()), + generate_locale_variables(), + flag_git_archive, + flag_rpkgs, + flag_tex_pkgs, + flag_rstudio, + shell_hook + ) + + } + + # Generate default.nix file + default.nix <- paste( + generate_header(rix_version, + nix_revision, + r_ver_text, + rix_call), + generate_rpkgs(cran_pkgs$rPackages), + generate_git_archived_packages(git_pkgs, cran_pkgs$archive_pkgs), + generate_tex_pkgs(tex_pkgs), + generate_system_pkgs(system_pkgs, r_pkgs), + generate_rstudio_pkgs(ide, flag_git_archive, flag_rpkgs), + generate_shell(flag_git_archive, flag_rpkgs), + collapse = "\n" + ) + + default.nix <- readLines(textConnection(default.nix)) + + if(print){ + cat(default.nix, sep = "\n") + } + + if(!file.exists(project_path) || overwrite){ + writeLines(default.nix, project_path) + } else { + stop(paste0("File exists at ", project_path, ". Set `overwrite == TRUE` to overwrite.")) + } + + + +} diff --git a/R/rix_init.R b/R/rix_init.R new file mode 100644 index 00000000..62456b24 --- /dev/null +++ b/R/rix_init.R @@ -0,0 +1,311 @@ +# WARNING - Generated by {fusen} from dev/flat_rix_init.Rmd: do not edit by hand + +#' Initiate and maintain an isolated, project-specific, and runtime-pure R +#' setup via Nix. +#' +#' Creates an isolated project folder for a Nix-R configuration. `rix::rix_init()` +#' also adds, appends, or updates with or without backup a custom `.Rprofile` +#' file with code that initializes a startup R environment without system's user +#' libraries within a Nix software environment. Instead, it restricts search +#' paths to load R packages exclusively from the Nix store. Additionally, it +#' makes Nix utilities like `nix-shell` available to run system commands from +#' the system's RStudio R session, for both Linux and macOS. +#' +#' **Enhancement of computational reproducibility for Nix-R environments:** +#' +#' The primary goal of `rix::rix_init()` is to enhance the computational +#' reproducibility of Nix-R environments during runtime. Notably, no restart is +#' required as environmental variables are set in the current session, in +#' addition to writing an `.Rprofile` file. This is particularly useful to make +#' [rix::with_nix()] evaluate custom R functions from any "Nix-to-Nix" or +#' "System-to-Nix" R setups. It introduces two side-effects that +#' take effect both in a current or later R session setup: +#' +#' 1. **Adjusting `R_LIBS_USER` path:** +#' By default, the first path of `R_LIBS_USER` points to the user library +#' outside the Nix store (see also [base::.libPaths()]). This creates +#' friction and potential impurity as R packages from the system's R user +#' library are loaded. While this feature can be useful for interactively +#' testing an R package in a Nix environment before adding it to a `.nix` +#' configuration, it can have undesired effects if not managed carefully. +#' A major drawback is that all R packages in the `R_LIBS_USER` location need +#' to be cleaned to avoid loading packages outside the Nix configuration. +#' Issues, especially on macOS, may arise due to segmentation faults or +#' incompatible linked system libraries. These problems can also occur +#' if one of the (reverse) dependencies of an R package is loaded along the +#' process. +#' +#' 2. **Make Nix commands available when running system commands from RStudio:** +#' In a host RStudio session not launched via Nix (`nix-shell`), the +#' environmental variables from `~/.zshrc` or `~/.bashrc` may not be +#' inherited. Consequently, Nix command line interfaces like `nix-shell` +#' might not be found. The `.Rprofile` code written by `rix::rix_init()` ensures +#' that Nix command line programs are accessible by adding the path of the +#' "bin" directory of the default Nix profile, +#' `"/nix/var/nix/profiles/default/bin"`, to the `PATH` variable in an +#' RStudio R session. +#' +#' These side effects are particularly recommended when working in flexible R +#' environments, especially for users who want to maintain both the system's +#' native R setup and utilize Nix expressions for reproducible development +#' environments. This init configuration is considered pivotal to enhance the +#' adoption of Nix in the R community, particularly until RStudio in Nixpkgs is +#' packaged for macOS. We recommend calling `rix::rix_init()` prior to comparing R +#' code ran between two software environments with `rix::with_nix()`. +#' +#' @param project_path Character with the folder path to the isolated nix-R project. +#' Defaults to `"."`, which is the current working directory path. If the folder +#' does not exist yet, it will be created. +#' @param rprofile_action Character. Action to take with `.Rprofile` file +#' destined for `project_path` folder. Possible values include +#' `"create_missing"`, which only writes `.Rprofile` if it +#' does not yet exist (otherwise does nothing); `"create_backup"`, which copies +#' the existing `.Rprofile` to a new backup file, generating names with +#' POSIXct-derived strings that include the time zone information. A new +#' `.Rprofile` file will be written with default code from `rix::rix_init()`; +#' `"overwrite"` overwrites the `.Rprofile` file if it does exist; `"append"` +#' appends the existing file with code that is tailored to an isolated Nix-R +#' project setup. +#' @param message_type Character. Message type, defaults to `"simple"`, which +#' gives minimal but sufficient feedback. Other values are currently +#' `"verbose"`, which provides more detailed diagnostics. +#' @export +#' @seealso [with_nix()] +#' @return Nothing, this function only has the side-effect of writing a file +#' called ".Rprofile" to the specified path. +#' @examples +#' \dontrun{ +#' # create an isolated, runtime-pure R setup via Nix +#' project_path <- "./sub_shell" +#' rix_init( +#' project_path = project_path, +#' rprofile_action = "create_missing" +#' ) +#' } +rix_init <- function(project_path = ".", + rprofile_action = c("create_missing", "create_backup", + "overwrite", "append"), + message_type = c("simple", "verbose")) { + message_type <- match.arg(message_type, choices = c("simple", "verbose")) + rprofile_action <- match.arg(rprofile_action, + choices = c("create_missing", "create_backup", "overwrite", "append")) + stopifnot( + "`project_path` needs to be character of length 1" = + is.character(project_path) && length(project_path) == 1L + ) + + cat("\n### Bootstrapping isolated, project-specific, and runtime-pure", + "R setup via Nix ###\n\n") + if (isFALSE(dir.exists(project_path))) { + dir.create(path = project_path, recursive = TRUE) + project_path <- normalizePath(path = project_path) + cat("==> Created isolated nix-R project folder:\n", project_path, "\n") + } else { + project_path <- normalizePath(path = project_path) + cat("==> Existing isolated nix-R project folder:\n", project_path, + "\n") + } + + # create project-local `.Rprofile` with pure settings + # first create the call, deparse it, and write it to .Rprofile + rprofile_quoted <- nix_rprofile() + rprofile_deparsed <- deparse_chr1(expr = rprofile_quoted, collapse = "\n") + rprofile_file <- file.path(project_path, ".Rprofile") + + rprofile_text <- get_rprofile_text(rprofile_deparsed) + write_rprofile <- function(rprofile_text, rprofile_file) { + writeLines( + text = rprofile_text, + con = file(rprofile_file) + ) + } + + is_nixr <- is_nix_rsession() + is_rstudio <- is_rstudio_session() + + rprofile_exists <- file.exists(rprofile_file) + timestamp <- format(Sys.time(), "%Y-%m-%dT%H:%M:%S%z") + rprofile_backup <- paste0(rprofile_file, "_backup_", timestamp) + + switch(rprofile_action, + create_missing = { + if (isTRUE(rprofile_exists)) { + cat( + "\n* Keep existing `.Rprofile`. in `project_path`:\n", + paste0(project_path, "/"), "\n" + ) + } else { + write_rprofile(rprofile_text, rprofile_file) + message_rprofile(action_string = "Added", project_path = project_path) + } + set_message_session_PATH(message_type = message_type) + }, + create_backup = { + if (isTRUE(rprofile_exists)) { + file.copy(from = rprofile_file, to = rprofile_backup) + cat( + "\n==> Backed up existing `.Rprofile` in file:\n", rprofile_backup, + "\n" + ) + write_rprofile(rprofile_text, rprofile_file) + message_rprofile( + action_string = "Overwrote", + project_path = project_path + ) + if (message_type == "verbose") { + cat("\n* Current lines of local `.Rprofile` are\n:") + cat(readLines(con = file(rprofile_file)), sep = "\n") + } + set_message_session_PATH(message_type = message_type) + } + }, + overwrite = { + write_rprofile(rprofile_text, rprofile_file) + if (isTRUE(rprofile_exists)) { + message_rprofile( + action_string = "Overwrote", project_path = project_path + ) + } else { + message_rprofile( + action_string = "Added", project_path = project_path + ) + } + }, + append = { + cat(paste0(rprofile_text, "\n"), file = rprofile_file, append = TRUE) + message_rprofile( + action_string = "Appended", project_path = project_path + ) + } + ) + + if (message_type == "verbose") { + cat("\n* Current lines of local `.Rprofile` are:\n\n") + cat(readLines(con = file(rprofile_file)), sep = "\n") + } + + on.exit(close(file(rprofile_file))) +} + +#' @noRd +get_rprofile_text <- function(rprofile_deparsed) { + c( +"### File generated by `rix::rix_init()` ### +# 1. Currently, system RStudio does not inherit environmental variables +# defined in `$HOME/.zshrc`, `$HOME/.bashrc` and alike. This is workaround to +# make the path of the nix store and hence basic nix commands available +# in an RStudio session +# 2. For nix-R session, remove `R_LIBS_USER`, system's R user library.`. +# This guarantees no user libraries from the system are loaded and only +# R packages in the Nix store are used. This makes Nix-R behave in pure manner +# at run-time.", + rprofile_deparsed + ) +} + +#' @noRd +message_rprofile <- function(action_string = "Added", + project_path = ".") { + msg <- paste0( + "\n==> ", action_string, + " `.Rprofile` file and code lines for new R sessions launched from:\n", + project_path, + "\n\n* Added the location of the Nix store to `PATH` ", + "environmental variable for new R sessions on host/docker RStudio:\n", + "/nix/var/nix/profiles/default/bin" + ) + cat(msg) +} + +#' @noRd +set_message_session_PATH <- function(message_type = c("simple", "verbose")) { + match.arg(message_type, choices = c("simple", "verbose")) + if (message_type == "verbose") { + cat("\n\n* Current `PATH` variable set in R session is:\n\n") + cat(Sys.getenv("PATH")) + } + cat("\n\n==> Also adjusting `PATH` via `Sys.setenv()`, so that", + "system commands can invoke key Nix commands like `nix-build` in this", + "RStudio session on the host operating system.") + PATH <- set_nix_path() + if (message_type == "verbose") { + cat("\n\n* Updated `PATH` variable is:\n\n", PATH) + } +} + +#' @noRd +is_nix_rsession <- function() { + is_nixr <- nzchar(Sys.getenv("NIX_STORE")) + if (isTRUE(is_nixr)) { + cat("==> R session running via Nix (nixpkgs)\n") + return(TRUE) + } else { + cat("\n==> R session running via host operating system or docker\n") + return(FALSE) + } +} + +#' @noRd +is_rstudio_session <- function() { + is_rstudio <- Sys.getenv("RSTUDIO") == "1" + if (isTRUE(is_rstudio)) { + cat("\n==> R session running from RStudio\n") + return(TRUE) + } else { + cat("* R session not running from RStudio") + return(FALSE) + } +} + +#' @noRd +set_nix_path <- function() { + old_path <- Sys.getenv("PATH") + nix_path <- "/nix/var/nix/profiles/default/bin" + has_nix_path <- any(grepl(nix_path, old_path)) + if (isFALSE(has_nix_path)) { + Sys.setenv( + PATH = paste(old_path, "/nix/var/nix/profiles/default/bin", sep = ":") + ) + } + invisible(Sys.getenv("PATH")) +} + +#' @noRd +nix_rprofile <- function() { + quote( { + is_rstudio <- Sys.getenv("RSTUDIO") == "1" + is_nixr <- nzchar(Sys.getenv("NIX_STORE")) + if (isFALSE(is_nixr) && isTRUE(is_rstudio)) { + # Currently, RStudio does not propagate environmental variables defined in + # `$HOME/.zshrc`, `$HOME/.bashrc` and alike. This is workaround to + # make the path of the nix store and hence basic nix commands available + # in an RStudio session + cat("{rix} detected RStudio R session") + old_path <- Sys.getenv("PATH") + nix_path <- "/nix/var/nix/profiles/default/bin" + has_nix_path <- any(grepl(nix_path, old_path)) + if (isFALSE(has_nix_path)) { + Sys.setenv( + PATH = paste( + old_path, nix_path, sep = ":" + ) + ) + } + rm(old_path, nix_path) + } + + if (isTRUE(is_nixr)) { + current_paths <- .libPaths() + userlib_paths <- Sys.getenv("R_LIBS_USER") + user_dir <- grep(paste(userlib_paths, collapse = "|"), current_paths) + new_paths <- current_paths[-user_dir] + # sets new library path without user library, making nix-R pure at + # run-time + .libPaths(new_paths) + rm(current_paths, userlib_paths, user_dir, new_paths) + } + + rm(is_rstudio, is_nixr) + } ) +} + diff --git a/R/with_nix.R b/R/with_nix.R new file mode 100644 index 00000000..275ea3b1 --- /dev/null +++ b/R/with_nix.R @@ -0,0 +1,827 @@ +# WARNING - Generated by {fusen} from dev/flat_with_nix.Rmd: do not edit by hand + + +#' Evaluate function in R or shell command via `nix-shell` environment +#' +#' This function needs an installation of Nix. `with_nix()` has two effects +#' to run code in isolated and reproducible environments. +#' 1. Evaluate a function in R or a shell command via the `nix-shell` +#' environment (Nix expression for custom software libraries; involving pinned +#' versions of R and R packages via Nixpkgs) +#' 2. If no error, return the result object of `expr` in `with_nix()` into the +#' current R session. +#' +#' +#' +#' `with_nix()` gives you the power of evaluating a main function `expr` +#' and its function call stack that are defined in the current R session +#' in an encapsulated nix-R session defined by Nix expression (`default.nix`), +#' which is located in at a distinct project path (`project_path`). +#' +#' `with_nix()` is very convenient because it gives direct code feedback in +#' read-eval-print-loop style, which gives a direct interface to the very +#' reproducible infrastructure-as-code approach offered by Nix and Nixpkgs. You +#' don't need extra efforts such as setting up DevOps tooling like Docker and +#' domain specific tools like {renv} to control complex software environments in +#' R and any other language. It is for example useful for the following +#' purposes. +#' +#' 1. test compatibility of custom R code and software/package dependencies in +#' development and production environments +#' 2. directly stream outputs (returned objects), messages and errors from any +#' command line tool offered in Nixpkgs into an R session. +#' 3. Test if evolving R packages change their behavior for given unchanged +#' R code, and whether they give identical results or not. +#' +#' `with_nix()` can evaluate both R code from a nix-R session within +#' another nix-R session, and also from a host R session (i.e., on macOS or +#' Linux) within a specific nix-R session. This feature is useful for testing +#' the reproducibility and compatibility of given code across different software +#' environments. If testing of different sets of environments is necessary, you +#' can easily do so by providing Nix expressions in custom `.nix` or +#' `default.nix` files in different subfolders of the project. +#' +#' To do its job, `with_nix()` heavily relies on patterns that manipulate +#' language expressions (aka computing on the language) offered in base R as +#' well as the {codetools} package by Luke Tierney. +#' +#' Some of the key steps that are done behind the scene: +#' 1. recursively find, classify, and export global objects (globals) in the +#' call stack of `expr` as well as propagate R package environments found. +#' 2. Serialize (save to disk) and deserialize (read from disk) dependent +#' data structures as `.Rds` with necessary function arguments provided, +#' any relevant globals in the call stack, packages, and `expr` outputs +#' returned in a temporary directory. +#' 3. Use pure `nix-shell` environments to execute a R code script +#' reconstructed catching expressions with quoting; it is launched by commands +#' like this via `{sys}` by Jeroen Ooms: +#' `nix-shell --pure --run "Rscript --vanilla"`. +#' +#' @param expr Single R function or call, or character vector of length one with +#' shell command and possibly options (flags) of the command to be invoked. +#' For `program = R`, you can both use a named or an anonymous function. +#' The function provided in `expr` should not evaluate when you pass arguments, +#' hence you need to wrap your function call like +#' `function() your_fun(arg_a = "a", arg_b = "b")`, to avoid evaluation and make +#' sure `expr` is a function (see details and examples). +#' @param program String stating where to evaluate the expression. Either `"R"`, +#' the default, or `"shell"`. `where = "R"` will evaluate the expression via +#' `RScript` and `where = "shell"` will run the system command in `nix-shell`. +#' @param exec_mode Either `"blocking"` (default) or `"non-blocking`. This +#' will either block the R session while `expr` is running in a `nix-shell` +#' environment, oor running it in the background ("non-blocking"). While +#' `program = R` will yield identical results for foreground and background +#' evaluation (R object), `program = "shell"` will return list of exit status, +#' standard output and standard error of the system command and as text in +#' blocking mode. +#' @param project_path Path to the folder where the `default.nix` file resides. +#' The default is `"."`, which is the working directory in the current R +#' session. This approach also useful when you have different subfolders +#' with separate software environments defined in different `default.nix` files. +#' If you prefer to run code in custom `.nix` files in the same directory +#' using `with_nix()`, you can use the `nix_file` argument to specify paths +#' to `.nix` files. +#' @param nix_file Path to `.nix` file that contains the expressions defining +#' the Nix software environment in which you want to run `expr`. See +#' `project_path` argument as an alternative way to specify the environment. +#' @param message_type String how detailed output is. Currently, there is +#' either `"simple"` (default) or `"verbose"`, which shows the script that runs +#' via `nix-shell`. +#' @importFrom codetools findGlobals checkUsage +#' @export +#' @return +#' - if `program = "R"`, R object returned by function given in `expr` +#' when evaluated via the R environment in `nix-shell` defined by Nix +#' expression. +#' - if `program = "shell"`, list with the following elements: +#' - `status`: exit code +#' - `stdout`: character vector with standard output +#' - `stderr`: character vector with standard error +#' of `expr` command sent to a command line interface provided by a Nix package. +#' @examples +#' \dontrun{ +#' # create an isolated, runtime-pure R setup via Nix +#' project_path <- "./sub_shell" +#' rix_init( +#' project_path = project_path, +#' rprofile_action = "create_missing" +#' ) +#' # generate nix environment in `default.nix` +#' rix( +#' r_ver = "4.2.0", +#' project_path = project_path +#' ) +#' # evaluate function in Nix-R environment via `nix-shell` and `Rscript`, +#' # stream messages, and bring output back to current R session +#' out <- with_nix( +#' expr = function(mtcars) nrow(mtcars), +#' program = "R", exec_mode = "non-blocking", project_path = project_path, +#' message_type = "simple" +#' ) +#' +#' # There no limit in the complexity of function call stacks that `with_nix()` +#' # can possibly handle; however, `expr` should not evaluate and +#' # needs to be a function for `program = "R"`. If you want to pass the +#' # a function with arguments, you can do like this +#' get_sample <- function(seed, n) { +#' set.seed(seed) +#' out <- sample(seq(1, 10), n) +#' return(out) +#' } +#' +#' out <- with_nix( +#' expr = get_sample(seed = 1234, n = 5), +#' program = "R", exec_mode = "non-blocking", +#' project_path = ".", +#' message_type = "simple" +#' ) +#' +#' ## You can also use packages, which will be exported to the nix-R session +#' ## running through `nix-shell` environment +#' R 4.2.2 +#' } +with_nix <- function(expr, + program = c("R", "shell"), + exec_mode = c("blocking", "non-blocking"), + project_path = ".", + nix_file = NULL, + message_type = c("simple", "verbose")) { + if (is.null(nix_file)) { + nix_file <- file.path(project_path, "default.nix") + } + stopifnot( + "`project_path` must be character of length 1." = + is.character(project_path) && length(project_path) == 1L, + "`project_path` has no `default.nix` file. Use one that contains `default.nix`" = + file.exists(nix_file), + "`message_type` must be character." = is.character(message_type), + "`expr` needs to be a call or function for `program = R`, and character of length 1 for `program = shell`" = + is.function(expr) || is.call(expr) || (is.character(expr) && length(expr) == 1L) + ) + + # ad-hoc solution for RStudio's limitation that R sessions cannot yet inherit + # proper `PATH` from custom `.Rprofile` on macOS (2023-01-17) + # adjust `PATH` to include `/nix/var/nix/profiles/default/bin` + if (isTRUE(is_rstudio_session()) && isFALSE(is_nix_rsession())) { + set_nix_path() + } + + has_nix_shell <- nix_shell_available() # TRUE if yes, FALSE if no + stopifnot("`nix-shell` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = + isTRUE(has_nix_shell)) + + if (isFALSE(has_nix_shell)) { + stop( + paste0("`nix-shell` is needed but is not available in your current ", + "shell environment.\n", + "* If you are in an R session of your host operating system, you + either\n1a) need to install Nix first, or if you have already done so ", + "\n", + "1b) make sure that the location of the nix store is in the `PATH` + variable of this R session (mostly necessary in RStudio).\n", + "* If you ran `with_nix()` from R launched in a `nix-shell`, you need + to make sure that `pkgs.nix` is in the `buildInput` for ", + "`pkgs.mkShell`.\nIf you used `rix::rix()` to generate your main nix + configuration of this session, just regenerate it with the additonal + argument `system_pkgs = 'nix'."), + call. = FALSE + ) + } + + program <- match.arg(program) + exec_mode <- match.arg(exec_mode) + message_type <- match.arg(message_type) + + if (program == "R") { + + # get the function arguments as a pairlist; + # save formal arguments of pairlist via `tag = value`; e.g., if we have a + # `expr = function(p = p_root) dir(path = p)`, the input object + # to be serialized will be serialized under `"p.Rds"` in a tmp dir, and + # will contain object `p_root`, which is defined in the global environment + # and bound to `"."` (project root) + args <- as.list(formals(expr)) + + cat("\n### Prepare to exchange arguments and globals for `expr`", + "between the host and Nix R sessions ###\n") + + # 1) save all function args onto a temporary folder each with + # `` and `value` as serialized objects from RAM --------------------- + temp_dir <- tempdir() + serialize_args(args, temp_dir) + + # cast list of symbols/names and calls to list of strings; this is to prepare + # deparsed version (string) of deserializing arguments from disk; + # elements of args for now should be of type "symbol" or "language" + args_vec <- vapply(args, deparse, FUN.VALUE = character(1L)) + + # todo in `rnix_deparsed`: + # => locate all global variables used by function + # https://github.com/cran/codetools/blob/master/R/codetools.R + # http://adv-r.had.co.nz/Expressions.html#ast-funs + + # code inspection: generates messages with potential problems + check_expr(expr) + + globals_expr <- recurse_find_check_globals(expr, args_vec) + + # wrapper around `serialize_lobjs()` + globals <- serialize_globals(globals_expr, temp_dir) + + # extract additional packages to export + pkgs <- serialize_pkgs(globals_expr, temp_dir) + + # 2) deserialize formal arguments of `expr` in nix session + # and necessary global objects --------------------------------------------- + # 3) serialize resulting output from evaluating function given as `expr` + + # main code to be run in nix R session + rnix_file <- file.path(temp_dir, "with_nix_r.R") + + rnix_quoted <- quote_rnix( + expr, program, message_type, args_vec, globals, pkgs, temp_dir, rnix_file + ) + rnix_deparsed <- deparse_chr1(expr = rnix_quoted, collapse = "\n") + + # 4): for 2) and 3) write script to disk, to run later via `Rscript` from + # `nix-shell` + # environment + r_version_file <- file.path(temp_dir, "nix-r-version.txt") + writeLines(text = rnix_deparsed, file(rnix_file)) + + # 3) run expression in nix session, based on temporary script + cat(paste0("==> Running deparsed expression via `nix-shell`", " in ", + exec_mode, " mode:\n\n"#, + # paste0(rnix_deparsed, collapse = " ") + )) + + # command to run deparsed R expression via nix-shell + cmd_rnix_deparsed <- c( + file.path(project_path, "default.nix"), + "--pure", # required for to have nix glibc + "--run", + sprintf( + "Rscript --vanilla '%s'", + rnix_file + ) + ) + + proc <- switch(exec_mode, + "blocking" = sys::exec_internal(cmd = "nix-shell", cmd_rnix_deparsed), + "non-blocking" = sys::exec_background( + cmd = "nix-shell", cmd_rnix_deparsed), + stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') + ) + if (exec_mode == "non-blocking") { + poll_sys_proc_nonblocking(cmd = cmd_rnix_deparsed, proc, what = "expr") + } else if (exec_mode == "blocking") { + poll_sys_proc_blocking(cmd = cmd_rnix_deparsed, proc, what = "expr") + } + } else if (program == "shell") { # end of `if (program == "R")` + shell_cmd <- c( + file.path(project_path, "default.nix"), + "--pure", + "--run", + expr + ) + proc <- switch(exec_mode, + "blocking" = sys::exec_internal(cmd = "nix-shell", shell_cmd), + "non-blocking" = sys::exec_background( + cmd = "nix-shell", shell_cmd), + stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') + ) + } + + # 5) deserialize final output of `expr` evaluated in nix-shell + # into host R session + if (program == "R") { + out <- readRDS(file = file.path(temp_dir, "_out.Rds")) + on.exit(close(file(rnix_file))) + } else if (program == "shell") { + if (exec_mode == "non-blocking") { + status <- poll_sys_proc_nonblocking( + cmd = shell_cmd, proc, what = "expr" + ) + out <- status + } else if (exec_mode == "blocking") { + poll_sys_proc_blocking(cmd = shell_cmd, proc, what = "expr") + out <- proc + out$stdout <- sys::as_text(out$stdout) + out$stderr <- sys::as_text(out$stderr) + } + } + + cat("\n### Finished code evaluation in `nix-shell` ###\n") + + # return output from evaluated function + cat("\n* Evaluating `expr` in `nix-shell` returns:\n") + if (program == "R") { + print(out) + } else if (program == "shell") { + print(out$stdout) + } + + cat("") + return(out) +} + + +#' serialize language objects +#' @noRd +serialize_lobjs <- function(lobjs, temp_dir) { + invisible({ + for (i in seq_along(lobjs)) { + if (!any(nzchar(deparse(lobjs[[i]])))) { + # for unnamed arguments like `expr = function(x) print(x)` + # x would be an empty symbol, see also ; i.e. arguments without + # default expressions; i.e. tagged arguments with no value + # https://stackoverflow.com/questions/3892580/create-missing-objects-aka-empty-symbols-empty-objects-needed-for-f + lobjs[[i]] <- as.symbol(names(lobjs)[i]) + } + saveRDS( + object = lobjs[[i]], + file = file.path(temp_dir, paste0(names(lobjs)[i], ".Rds")) + ) + } + }) +} + +serialize_args <- function(args, temp_dir) { + invisible({ + for (i in seq_along(args)) { + if (!nzchar(deparse(args[[i]]))) { + # for unnamed arguments like `expr = function(x) print(x)` + # x would be an empty symbol, see also ; i.e. arguments without + # default expressions; i.e., tagged arguments with no value + # https://stackoverflow.com/questions/3892580/create-missing-objects-aka-empty-symbols-empty-objects-needed-for-f + args[[i]] <- as.symbol(names(args)[i]) + } + args[[i]] <- get(as.character(args[[i]])) + saveRDS( + object = args[[i]], + file = file.path(temp_dir, paste0(names(args)[i], ".Rds")) + ) + } + }) +} + + +#' @noRd +check_expr <- function(expr) { + cat("* checking code in `expr` for potential problems:\n", + "`codetools::checkUsage(fun = expr)`\n") + codetools::checkUsage(fun = expr) + cat("\n") + } + + +#' @noRd +# to determine which extra packages to load in Nix R prior evaluating `expr` +get_expr_extra_pkgs <- function(globals_expr) { + envs_check <- lapply(globals_expr, where) + names_envs_check <- vapply(envs_check, environmentName, character(1L)) + + default_pkgnames <- paste0("package:", getOption("defaultPackages")) + pkgenvs_attached <- setdiff( + grep("^package:", names_envs_check, value = TRUE), + c(default_pkgnames, "base") + ) + if (!length(pkgenvs_attached) == 0L) { + pkgs_to_attach <- gsub("^package:", "", pkgenvs_attached) + return(pkgs_to_attach) + } else { + return(NULL) + } +} + + +#' @noRd +is_empty <- function(x) identical(x, emptyenv()) + + +#' @noRd +where <- function(name, env = parent.frame()) { + while(!is_empty(env)) { + if (exists(name, envir = env, inherits = FALSE)) { + return(env) + } + # inspect parent + env <- parent.env(env) + } +} + +#' Finds and checks global functions and variables recursively for closure +#' `expr` +#' @noRd +recurse_find_check_globals <- function(expr, args_vec) { + + cat("* checking code in `expr` for potential problems:\n") + codetools::checkUsage(fun = expr) + cat("\n") + + globals_expr <- codetools::findGlobals(fun = expr) + globals_lst <- classify_globals(globals_expr, args_vec) + + round_i <- 1L + + repeat { + + get_globals_exprs <- function(globals_lst) { + globals_exprs <- names(unlist(Filter(function(x) !is.null(x), + unname(globals_lst[c("globalenv_fun", "env_fun")])))) + return(globals_exprs) + } + + if (round_i == 1L) { + # first round + globals_exprs <- get_globals_exprs(globals_lst) + } else { + # successive rounds + globals_exprs <- unlist(lapply(globals_lst, get_globals_exprs)) + } + + cat("* checking code in `globals_exprs` for potential problems:\n") + lapply( + globals_exprs, + codetools::checkUsage + ) + cat("\n") + + globals_new <- lapply( + globals_exprs, + function(x) codetools::findGlobals(fun = x) + ) + + globals_lst_new <- lapply( + globals_new, + function(x) classify_globals(globals_expr = x, args_vec) + ) + + if (round_i == 1L) { + result_list <- c(list(globals_lst), globals_lst_new) + } else { + result_list <- c(result_list, globals_lst_new) + } + + # prepare current globals to find new globals one recursion level deeper + # in the call stack in the next repeat + globals_lst <- globals_lst_new + + globals_lst <- lapply(globals_lst, function(x) lapply(x, unlist)) + + # packages need to be excluded for getting more globals + globals_lst <- lapply( + globals_lst, + function(x) { + x[c("globalenv_fun", "globalenv_other", "env_other", "env_fun")] + } + ) + + globals_null <- all(is.null(unlist(globals_lst))) + # TRUE if no more candidate global values + all_non_pkgs_null <- all(globals_null) + + round_i <- round_i + 1L + + if (is.null(globals_lst) || all_non_pkgs_null) break + } + + result_list <- Filter(function(x) !is.null(x), result_list) + result_list <- lapply( + result_list, + function(x) Filter(function(x) !is.null(x), x) + ) + + pkgs <- unlist(lapply(result_list, "[", "pkgs")) + + unlist_unname <- function(x) { + unlist( + lapply(x, function(x) unlist(unname(x))) + ) + } + + globalenv_fun <- lapply(result_list, "[", "globalenv_fun") + globalenv_fun <- unlist_unname(globalenv_fun) + + globalenv_other <- lapply(result_list, "[", "globalenv_other") + globalenv_other <- unlist_unname(globalenv_other) + + env_other <- lapply(result_list, "[", "env_other") + env_other <- unlist_unname(env_other) + + env_fun = lapply(result_list, "[", "env_fun") + env_fun <- unlist_unname(env_fun) + + exports <- list( + pkgs = pkgs, + globalenv_fun = globalenv_fun, + globalenv_other = globalenv_other, + env_other = env_other, + env_fun = env_fun + ) + + return(exports) +} + +#' @noRd +classify_globals <- function(globals_expr, args_vec) { + envs_check <- lapply(globals_expr, where) + names(envs_check) <- globals_expr + + vec_envs_check <- vapply(envs_check, environmentName, character(1L)) + # directly remove formals + vec_envs_check <- vec_envs_check[!names(vec_envs_check) %in% args_vec] + if (length(vec_envs_check) == 0L) { + vec_envs_check <- NULL + } + + if (!is.null(vec_envs_check)) { + globs_pkg <- grep("^package:", vec_envs_check, value = TRUE) + if (length(globs_pkg) == 0L) { + globs_pkg <- NULL + } + # globs base can be ignored + globs_base <- grep("^base$", vec_envs_check, value = TRUE) + globs_globalenv <- grep("^R_GlobalEnv$", vec_envs_check, value = TRUE) + globs_globalenv <- Filter(nzchar, globs_globalenv) + # empty globs; can be ignored for now + globs_empty <- Filter(function(x) !nzchar(x), vec_envs_check) + if (length(globs_empty) == 0L) { + globs_empty <- NULL + } + globs_other <- vec_envs_check[!names(vec_envs_check) %in% + names(c(globs_pkg, globs_globalenv, globs_empty, globs_base))] + if (length(globs_other) == 0L) { + globs_other <- NULL + } + } + + is_globalenv_funs <- vapply( + names(globs_globalenv), function(x) is.function(get(x)), + FUN.VALUE = logical(1L) + ) + + is_otherenv_funs <- vapply( + names(globs_other), function(x) is.function(get(x)), + FUN.VALUE = logical(1L) + ) + + globs_globalenv_fun <- globs_globalenv[is_globalenv_funs] + if (length(globs_globalenv_fun) == 0L) { + globs_globalenv_fun <- NULL + } + globs_globalenv_other <- globs_globalenv[!is_globalenv_funs] + if (length(globs_globalenv_other) == 0L) { + globs_globalenv_other <- NULL + } + + globs_otherenv_fun <- globs_other[is_otherenv_funs] + if (length(globs_otherenv_fun) == 0L) { + globs_otherenv_fun <- NULL + } + globs_otherenv_other <- globs_other[!is_otherenv_funs] + if (length(globs_otherenv_other) == 0L) { + globs_otherenv_other <- NULL + } + + default_pkgnames <- paste0("package:", getOption("defaultPackages")) + pkgenvs_attached <- setdiff(globs_pkg, c(default_pkgnames, "base")) + + if (!length(pkgenvs_attached) == 0L) { + pkgs_to_attach <- gsub("^package:", "", pkgenvs_attached) + } else { + pkgs_to_attach <- NULL + } + + globs_classified <- list( + globalenv_fun = globs_globalenv_fun, + globalenv_other = globs_globalenv_other, + env_other = globs_otherenv_other, + env_fun = globs_otherenv_fun, + pkgs = pkgs_to_attach + ) + globs_null <- all(vapply(globs_classified, is.null, logical(1L))) + if (globs_null) globs_classified <- NULL + + return(globs_classified) +} + + +# wrapper to serialize expressions of all global objects found +#' @noRd +serialize_globals <- function(globals_expr, temp_dir) { + funs <- globals_expr$globalenv_fun + if (!is.null(funs)) { + cat("=> Saving global functions to disk:", paste(names(funs)), "\n") + globalenv_funs <- lapply( + names(funs), + function(x) get(x = x, envir = .GlobalEnv) + ) + names(globalenv_funs) <- names(globals_expr$globalenv_fun) + serialize_lobjs(lobjs = globalenv_funs, temp_dir) + } + others <- globals_expr$globalenv_other + if (!is.null(others)) { + cat("=> Saving non-function object(s), e.g. other environments:", + paste(names(others)), "\n" + ) + globalenv_others <- lapply( + names(others), + function(x) get(x = x, envir = .GlobalEnv) + ) + names(globalenv_others) <- names(globals_expr$globalenv_other) + serialize_lobjs(lobjs = globalenv_others, temp_dir) + } + env_funs <- globals_expr$env_fun + if (!is.null(env_funs)) { + cat("=> Serializing function(s) from other environment(s):", + paste(names(env_funs)), "\n") + env_funs <- lapply( + names(env_funs), + function(x) get(x = x) + ) + names(env_funs) <- names(globals_expr$env_fun) + serialize_lobjs(lobjs = env_funs, temp_dir) + } + env_others <- globals_expr$env_other + if (!is.null(env_others)) { + cat("=> Serializing non-function object(s) from custom environment(s)::", + paste(names(env_others)), "\n" + ) + env_others <- lapply( + names(env_others), + function(x) get(x = x) + ) + names(env_others) <- names(globals_expr$env_other) + serialize_lobjs(lobjs = env_others, temp_dir) + } + + return(c(funs, others, env_funs, env_others)) +} + + +#' @noRd +serialize_pkgs <- function(globals_expr, temp_dir) { + pkgs <- globals_expr$pkgs + if (!is.null(pkgs)) { + cat("=> Serializing package(s) required to run `expr`:\n", + paste(pkgs), "\n" + ) + } + saveRDS( + object = pkgs, + file = file.path(temp_dir, "_pkgs.Rds") + ) + return(pkgs) +} + +# build deparsed script via language objects; +# reads like R code, and avoids code injection +quote_rnix <- function(expr, + program, + message_type, + args_vec, + globals, + pkgs, + temp_dir, + rnix_file) { + expr_quoted <- bquote( { + cat("### Start evaluating `expr` in `nix-shell` ###") + cat("\n* wrote R script evaluated via `Rscript` in `nix-shell`:", + .(rnix_file)) + temp_dir <- .(temp_dir) + cat("\n", Sys.getenv("NIX_PATH")) + # fix library paths for nix R on macOS and linux; avoid permission issue + current_paths <- .libPaths() + userlib_paths <- Sys.getenv("R_LIBS_USER") + user_dir <- grep(paste(userlib_paths, collapse = "|"), current_paths) + new_paths <- current_paths[-user_dir] + .libPaths(new_paths) + r_version_num <- paste0(R.version$major, ".", R.version$minor) + cat("\n* using Nix with R version", r_version_num, "\n\n") + # assign `args_vec` as in c(...) form. + args_vec <- .(with_assign_vecnames_call(vec = args_vec)) + # deserialize arguments from disk + for (i in seq_along(args_vec)) { + nm <- args_vec[i] + obj <- args_vec[i] + assign( + x = nm, + value = readRDS(file = file.path(temp_dir, paste0(obj, ".Rds"))) + ) + cat( + paste0(" => reading file ", "'", obj, ".Rds", "'", + " for argument named `", obj, "`\n") + ) + } + + globals <- .(with_assign_vecnames_call(vec = globals)) + for (i in seq_along(globals)) { + nm <- globals[i] + obj <- globals[i] + assign( + x = nm, + value = readRDS(file = file.path(temp_dir, paste0(obj, ".Rds"))) + ) + cat( + paste0(" => reading file ", "'", obj, ".Rds", "'", + " for global object named `", obj, "`\n") + ) + } + + # for now name of character vector containing packages is hard-coded + # pkgs <- .(with_assign_vecnames_call(vec = pkgs)) + # pkgs <- .(pkgs) + pkgs <- .(with_assign_vec_call(vec = pkgs)) + lapply(pkgs, library, character.only = TRUE) + + # execute function call in `expr` with list of correct args + lst <- as.list(args_vec) + names(lst) <- args_vec + lst <- lapply(lst, as.name) + rnix_out <- do.call(.(expr), lst) + cat("\n* called `expr` with args", args_vec, ":\n") + message_type <- .(message_type) + if (message_type == "verbose") { + # cat("\n", deparse(.(expr))) # not nicely formatted, use print + # print(.(expr)) + } + cat("\n* The type of the output object returned by `expr` is", + typeof(rnix_out)) + saveRDS(object = rnix_out, file = file.path(temp_dir, "_out.Rds")) + cat("\n* Saved output to", file.path(temp_dir, "_out.Rds")) + cat("\n\n* the following objects are in the global environment:\n") + cat(ls()) + cat("\n") + cat("\n* `sessionInfo()` output:\n") + cat(capture.output(sessionInfo()), sep = "\n") + } ) # end of `bquote()` + + return(expr_quoted) +} + +# https://github.com/cran/codetools/blob/master/R/codetools.R +# finding global variables + +# reconstruct argument vector (character) in Nix R; +# build call to generate `args_vec` +#' @noRd +with_assign_vecnames_call <- function(vec) { + cl <- call("c") + for (i in seq_along(vec)) { + cl[[i + 1L]] <- names(vec[i]) + } + return(cl) +} + +#' @noRd +with_assign_vec_call <- function(vec) { + cl <- call("c") + for (i in seq_along(vec)) { + cl[[i + 1L]] <- vec[i] + } + return(cl) +} + +# this is what `deparse1()` does, however, it is only since 4.0.0 +#' @noRd +deparse_chr1 <- function(expr, width.cutoff = 500L, collapse = " ", ...) { + paste(deparse(expr, width.cutoff, ...), collapse = collapse) +} + +#' @noRd +with_expr_deparse <- function(expr) { + sprintf( + 'run_expr <- %s\n', + deparse_chr1(expr = expr, collapse = "\n") + ) +} + +#' @noRd +nix_shell_available <- function() { + which_nix_shell <- Sys.which("nix-shell") + if (nzchar(which_nix_shell)) { + return(TRUE) + } else { + return(FALSE) + } +} + +#' @noRd +create_shell_nix <- function(path = file.path("inst", "extdata", + "with_nix", "default.nix")) { + if (!dir.exists(dirname(path))) { + dir.create(dirname(path), recursive = TRUE) + } + + rix( + r_ver = "latest", + r_pkgs = NULL, + system_pkgs = NULL, + git_pkgs = NULL, + ide = "other", + project_path = dirname(path), + overwrite = TRUE, + shell_hook = NULL + ) +} diff --git a/dev/0-dev_history.Rmd b/dev/0-dev_history.Rmd index 70cb579c..bd469956 100755 --- a/dev/0-dev_history.Rmd +++ b/dev/0-dev_history.Rmd @@ -101,20 +101,44 @@ fusen::inflate(flat_file = "dev/flat_data_doc.Rmd", ``` ```{r} -fusen::inflate(flat_file = "dev/flat_build_envs.Rmd", +fusen::inflate(flat_file = "dev/flat_available_R.Rmd", vignette_name = NA, overwrite = TRUE) ``` + ```{r} fusen::inflate(flat_file = "dev/flat_cicd.Rmd", vignette_name = NA, overwrite = TRUE) ``` +```{r} +fusen::inflate(flat_file = "dev/flat_cran_archive.Rmd", + vignette_name = NA, + overwrite = TRUE) +``` ```{r} -fusen::inflate(flat_file = "dev/flat_zzz.Rmd", +fusen::inflate(flat_file = "dev/flat_data_doc.Rmd", + vignette_name = NA, + overwrite = TRUE) +``` + +```{r} +fusen::inflate(flat_file = "dev/flat_fetchers.Rmd", + vignette_name = NA, + overwrite = TRUE) +``` + +```{r} +fusen::inflate(flat_file = "dev/flat_find_rev.Rmd", + vignette_name = NA, + overwrite = TRUE) +``` + +```{r} +fusen::inflate(flat_file = "dev/flat_get_latest.Rmd", vignette_name = NA, overwrite = TRUE) ``` @@ -126,7 +150,38 @@ fusen::inflate(flat_file = "dev/flat_get_os.Rmd", ``` ```{r} -fusen::inflate(flat_file = "dev/flat_cran_archive.Rmd", +fusen::inflate(flat_file = "dev/flat_get_sri_hash_deps.Rmd", + vignette_name = NA, + overwrite = TRUE) +``` + +```{r} +fusen::inflate(flat_file = "dev/flat_nix_build.Rmd", + vignette_name = NA, + overwrite = TRUE) +``` + +```{r} +fusen::inflate(flat_file = "dev/flat_rix_init.Rmd", + vignette_name = NA, + overwrite = TRUE) +``` + +```{r} +fusen::inflate(flat_file = "dev/flat_with_nix.Rmd", + vignette_name = NA, + overwrite = TRUE) +``` + + +```{r} +fusen::inflate(flat_file = "dev/flat_rix.Rmd", + vignette_name = NA, + overwrite = TRUE) +``` + +```{r} +fusen::inflate(flat_file = "dev/flat_zzz.Rmd", vignette_name = NA, overwrite = TRUE) ``` @@ -134,68 +189,68 @@ fusen::inflate(flat_file = "dev/flat_cran_archive.Rmd", # Vignettes for users ```{r} -fusen::inflate(flat_file = "dev/1-getting_started.Rmd", +fusen::inflate(flat_file = "dev/a-getting_started.Rmd", vignette_name = "a - Getting started", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/2a-linux_win.Rmd", +fusen::inflate(flat_file = "dev/b1-linux_win.Rmd", vignette_name = "b1 - Setting up and using rix on Linux and Windows", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/2b-macos.Rmd", +fusen::inflate(flat_file = "dev/b2-macos.Rmd", vignette_name = "b2 - Setting up and using rix on macOS", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/3-building_envs_with_rix.Rmd", +fusen::inflate(flat_file = "dev/c-building_envs_with_rix.Rmd", vignette_name = "c - Using rix to build project specific environments", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/4a-install_r_pkgs.Rmd", +fusen::inflate(flat_file = "dev/d1-install_r_pkgs.Rmd", vignette_name = "d1 - Installing R packages in a Nix environment", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/4b-install_sys_pkgs.Rmd", +fusen::inflate(flat_file = "dev/d2-install_sys_pkgs.Rmd", vignette_name = "d2 - Installing system tools and TexLive packages in a Nix environment", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/5-interactive_use.Rmd", +fusen::inflate(flat_file = "dev/e-interactive_use.Rmd", vignette_name = "e - Interactive use", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/literate_programming.Rmd", +fusen::inflate(flat_file = "dev/z-literate_programming.Rmd", vignette_name = "z - Advanced topic: Building an environment for literate programming", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/pkgs_with_remotes.Rmd", +fusen::inflate(flat_file = "dev/z-pkgs_with_remotes.Rmd", vignette_name = "z - Advanced topic: Handling packages with remote dependencies", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/raps_with_nix.Rmd", +fusen::inflate(flat_file = "dev/z-raps_with_nix.Rmd", vignette_name = "z - Advanced topic: Reproducible Analytical Pipelines with Nix", overwrite = TRUE) ``` ```{r} -fusen::inflate(flat_file = "dev/subshells.Rmd", +fusen::inflate(flat_file = "dev/z-subshells.Rmd", vignette_name = "z - Advanced topic: Running R or Shell Code in Nix from R", overwrite = TRUE) ``` @@ -203,14 +258,14 @@ fusen::inflate(flat_file = "dev/subshells.Rmd", # Generating the inst/extdata/default.nix ```{r, eval = FALSE} -rix(r_ver = "976fa3369d722e76f37c77493d99829540d43845", +rix(r_ver = "latest", r_pkgs = NULL, system_pkgs = NULL, git_pkgs = list( package_name = "rix", repo_url = "https://github.com/b-rodrigues/rix/", branch_name = "master", - commit = "ae39d2142461688b1be41db800752a949ebb3c7b" + commit = "11f6898fde38a4793a7f53900c88d2fd930e882f" ), ide = "other", project_path = "inst/extdata", diff --git a/dev/1-getting_started.Rmd b/dev/a-getting_started.Rmd similarity index 99% rename from dev/1-getting_started.Rmd rename to dev/a-getting_started.Rmd index a85bb808..20da9125 100644 --- a/dev/1-getting_started.Rmd +++ b/dev/a-getting_started.Rmd @@ -1,5 +1,5 @@ --- -title: "1 - Getting started" +title: "a - Getting started" output: html_document editor_options: chunk_output_type: console diff --git a/dev/2a-linux_win.Rmd b/dev/b1-linux_win.Rmd similarity index 98% rename from dev/2a-linux_win.Rmd rename to dev/b1-linux_win.Rmd index f81cd068..01dacb45 100644 --- a/dev/2a-linux_win.Rmd +++ b/dev/b1-linux_win.Rmd @@ -1,5 +1,5 @@ --- -title: "2a - Setting up and using rix on Linux and Windows" +title: "b1 - Setting up and using rix on Linux and Windows" output: html_document editor_options: chunk_output_type: console diff --git a/dev/2b-macos.Rmd b/dev/b2-macos.Rmd similarity index 92% rename from dev/2b-macos.Rmd rename to dev/b2-macos.Rmd index aa3fc5e5..c48a8441 100644 --- a/dev/2b-macos.Rmd +++ b/dev/b2-macos.Rmd @@ -1,5 +1,5 @@ --- -title: "2b - Setting up and using rix on macOS" +title: "b2 - Setting up and using rix on macOS" output: html_document editor_options: chunk_output_type: console @@ -22,14 +22,13 @@ idiosyncracies. This vignette details these. ## Installing Nix -You can use `{rix}` to generate Nix expressions even if you don't have Nix installed -on your system, but obviously, you need to install Nix if you actually want to -build the defined development environment and use them. Installing (and uninstalling) -Nix is quite simple, thanks to the installer from -[Determinate +You can use `{rix}` to generate Nix expressions even if you don't have Nix +installed on your system, but obviously, you need to install Nix if you actually +want to build the defined development environment and use them. Installing (and +uninstalling) Nix is quite simple, thanks to the installer from [Determinate Systems](https://determinate.systems/posts/determinate-nix-installer), a company -that provides services and tools built on Nix. Simply open a terminal and run the following -line: +that provides services and tools built on Nix. Simply open a terminal and run +the following line: ```{sh, eval=FALSE} curl --proto '=https' --tlsv1.2 -sSf \ @@ -37,7 +36,8 @@ curl --proto '=https' --tlsv1.2 -sSf \ sh -s -- install ``` -Once you have Nix installed, you can build the expressions you generate with `{rix}`! +Once you have Nix installed, you can build the expressions you generate with +`{rix}`! ## What if you don't have R already installed? @@ -68,7 +68,8 @@ rix(r_ver = "latest", to generate a `default.nix`, and then use that file to generate an environment with R, `{dplyr}` and `{ggplot2}`. If you need to add packages for your project, rerun the command above, but add the needed packages to `r_pkgs`. This is -detailled in the vignette `vignette("d1-installing-r-packages-in-a-nix-environment")` and +detailled in the vignette +`vignette("d1-installing-r-packages-in-a-nix-environment")` and `vignette("d2-installing-system-tools-and-texlive-packages-in-a-nix-environment")`. ## Generating expressions diff --git a/dev/3-building_envs_with_rix.Rmd b/dev/c-building_envs_with_rix.Rmd similarity index 99% rename from dev/3-building_envs_with_rix.Rmd rename to dev/c-building_envs_with_rix.Rmd index a9b9c834..674462d2 100644 --- a/dev/3-building_envs_with_rix.Rmd +++ b/dev/c-building_envs_with_rix.Rmd @@ -1,5 +1,5 @@ --- -title: "3 - Using rix to build project specific environments" +title: "c - Using rix to build project specific environments" output: html_document editor_options: chunk_output_type: console diff --git a/dev/config_fusen.yaml b/dev/config_fusen.yaml index eaba386e..b4492112 100644 --- a/dev/config_fusen.yaml +++ b/dev/config_fusen.yaml @@ -1,102 +1,102 @@ -1-getting_started.Rmd: - path: dev/1-getting_started.Rmd +a-getting_started.Rmd: + path: dev/a-getting_started.Rmd state: active R: [] tests: [] vignettes: vignettes/a-getting-started.Rmd inflate: - flat_file: dev/1-getting_started.Rmd + flat_file: dev/a-getting_started.Rmd vignette_name: a - Getting started open_vignette: true check: true document: true overwrite: 'yes' -2a-linux_win.Rmd: - path: dev/2a-linux_win.Rmd +b1-linux_win.Rmd: + path: dev/b1-linux_win.Rmd state: active R: [] tests: [] vignettes: vignettes/b1-setting-up-and-using-rix-on-linux-and-windows.Rmd inflate: - flat_file: dev/2a-linux_win.Rmd + flat_file: dev/b1-linux_win.Rmd vignette_name: b1 - Setting up and using rix on Linux and Windows open_vignette: true check: true document: true overwrite: 'yes' -2b-macos.Rmd: - path: dev/2b-macos.Rmd +b2-macos.Rmd: + path: dev/b2-macos.Rmd state: active R: [] tests: [] vignettes: vignettes/b2-setting-up-and-using-rix-on-macos.Rmd inflate: - flat_file: dev/2b-macos.Rmd + flat_file: dev/b2-macos.Rmd vignette_name: b2 - Setting up and using rix on macOS open_vignette: true check: true document: true overwrite: 'yes' -3-building_envs_with_rix.Rmd: - path: dev/3-building_envs_with_rix.Rmd +c-building_envs_with_rix.Rmd: + path: dev/c-building_envs_with_rix.Rmd state: active R: [] tests: [] vignettes: vignettes/c-using-rix-to-build-project-specific-environments.Rmd inflate: - flat_file: dev/3-building_envs_with_rix.Rmd + flat_file: dev/c-building_envs_with_rix.Rmd vignette_name: c - Using rix to build project specific environments open_vignette: true check: true document: true overwrite: 'yes' -4a-install_r_pkgs.Rmd: - path: dev/4a-install_r_pkgs.Rmd +d1-install_r_pkgs.Rmd: + path: dev/d1-install_r_pkgs.Rmd state: active R: [] tests: [] vignettes: vignettes/d1-installing-r-packages-in-a-nix-environment.Rmd inflate: - flat_file: dev/4a-install_r_pkgs.Rmd + flat_file: dev/d1-install_r_pkgs.Rmd vignette_name: d1 - Installing R packages in a Nix environment open_vignette: true check: true document: true overwrite: 'yes' -4b-install_sys_pkgs.Rmd: - path: dev/4b-install_sys_pkgs.Rmd +d2-install_sys_pkgs.Rmd: + path: dev/d2-install_sys_pkgs.Rmd state: active R: [] tests: [] vignettes: vignettes/d2-installing-system-tools-and-texlive-packages-in-a-nix-environment.Rmd inflate: - flat_file: dev/4b-install_sys_pkgs.Rmd + flat_file: dev/d2-install_sys_pkgs.Rmd vignette_name: d2 - Installing system tools and TexLive packages in a Nix environment open_vignette: true check: true document: true overwrite: 'yes' -5-interactive_use.Rmd: - path: dev/5-interactive_use.Rmd +e-interactive_use.Rmd: + path: dev/e-interactive_use.Rmd state: active R: [] tests: [] vignettes: vignettes/e-interactive-use.Rmd inflate: - flat_file: dev/5-interactive_use.Rmd + flat_file: dev/e-interactive_use.Rmd vignette_name: e - Interactive use open_vignette: true check: true document: true overwrite: 'yes' -flat_build_envs.Rmd: - path: dev/flat_build_envs.Rmd +flat_available_R.Rmd: + path: dev/flat_available_R.Rmd state: active - R: R/find_rev.R - tests: tests/testthat/test-find_rev.R + R: R/available_r.R + tests: tests/testthat/test-available_r.R vignettes: [] inflate: - flat_file: dev/flat_build_envs.Rmd + flat_file: dev/flat_available_R.Rmd vignette_name: .na open_vignette: true check: true @@ -141,6 +141,45 @@ flat_data_doc.Rmd: check: true document: true overwrite: 'yes' +flat_fetchers.Rmd: + path: dev/flat_fetchers.Rmd + state: active + R: R/fetchgit.R + tests: [] + vignettes: [] + inflate: + flat_file: dev/flat_fetchers.Rmd + vignette_name: .na + open_vignette: true + check: true + document: true + overwrite: 'yes' +flat_find_rev.Rmd: + path: dev/flat_find_rev.Rmd + state: active + R: R/find_rev.R + tests: tests/testthat/test-find_rev.R + vignettes: [] + inflate: + flat_file: dev/flat_find_rev.Rmd + vignette_name: .na + open_vignette: true + check: true + document: true + overwrite: 'yes' +flat_get_latest.Rmd: + path: dev/flat_get_latest.Rmd + state: active + R: R/get_latest.R + tests: [] + vignettes: [] + inflate: + flat_file: dev/flat_get_latest.Rmd + vignette_name: .na + open_vignette: true + check: true + document: true + overwrite: 'yes' flat_get_os.Rmd: path: dev/flat_get_os.Rmd state: active @@ -154,6 +193,58 @@ flat_get_os.Rmd: check: true document: true overwrite: 'yes' +flat_get_sri_hash_deps.Rmd: + path: dev/flat_get_sri_hash_deps.Rmd + state: active + R: R/get_sri_hash_deps.R + tests: tests/testthat/test-get_sri_hash_deps.R + vignettes: [] + inflate: + flat_file: dev/flat_get_sri_hash_deps.Rmd + vignette_name: .na + open_vignette: true + check: true + document: true + overwrite: 'yes' +flat_nix_build.Rmd: + path: dev/flat_nix_build.Rmd + state: active + R: R/nix_build.R + tests: [] + vignettes: [] + inflate: + flat_file: dev/flat_nix_build.Rmd + vignette_name: .na + open_vignette: true + check: true + document: true + overwrite: 'yes' +flat_rix.Rmd: + path: dev/flat_rix.Rmd + state: active + R: R/rix.R + tests: tests/testthat/test-rix.R + vignettes: [] + inflate: + flat_file: dev/flat_rix.Rmd + vignette_name: .na + open_vignette: true + check: true + document: true + overwrite: 'yes' +flat_rix_init.Rmd: + path: dev/flat_rix_init.Rmd + state: active + R: R/rix_init.R + tests: tests/testthat/test-rix_init.R + vignettes: [] + inflate: + flat_file: dev/flat_rix_init.Rmd + vignette_name: .na + open_vignette: true + check: true + document: true + overwrite: 'yes' flat_save_r_nix_revs.Rmd: path: dev/flat_save_r_nix_revs.Rmd state: active @@ -167,6 +258,19 @@ flat_save_r_nix_revs.Rmd: check: true document: true overwrite: 'yes' +flat_with_nix.Rmd: + path: dev/flat_with_nix.Rmd + state: active + R: R/with_nix.R + tests: tests/testthat/test-with_nix.R + vignettes: [] + inflate: + flat_file: dev/flat_with_nix.Rmd + vignette_name: .na + open_vignette: true + check: true + document: true + overwrite: 'yes' flat_zzz.Rmd: path: dev/flat_zzz.Rmd state: active @@ -180,53 +284,53 @@ flat_zzz.Rmd: check: true document: true overwrite: 'yes' -literate_programming.Rmd: - path: dev/literate_programming.Rmd +z-literate_programming.Rmd: + path: dev/z-literate_programming.Rmd state: active R: [] tests: [] vignettes: vignettes/z-advanced-topic-building-an-environment-for-literate-programming.Rmd inflate: - flat_file: dev/literate_programming.Rmd + flat_file: dev/z-literate_programming.Rmd vignette_name: 'z - Advanced topic: Building an environment for literate programming' open_vignette: true check: true document: true overwrite: 'yes' -pkgs_with_remotes.Rmd: - path: dev/pkgs_with_remotes.Rmd +z-pkgs_with_remotes.Rmd: + path: dev/z-pkgs_with_remotes.Rmd state: active R: [] tests: [] vignettes: vignettes/z-advanced-topic-handling-packages-with-remote-dependencies.Rmd inflate: - flat_file: dev/pkgs_with_remotes.Rmd + flat_file: dev/z-pkgs_with_remotes.Rmd vignette_name: 'z - Advanced topic: Handling packages with remote dependencies' open_vignette: true check: true document: true overwrite: 'yes' -raps_with_nix.Rmd: - path: dev/raps_with_nix.Rmd +z-raps_with_nix.Rmd: + path: dev/z-raps_with_nix.Rmd state: active R: [] tests: [] vignettes: vignettes/z-advanced-topic-reproducible-analytical-pipelines-with-nix.Rmd inflate: - flat_file: dev/raps_with_nix.Rmd + flat_file: dev/z-raps_with_nix.Rmd vignette_name: 'z - Advanced topic: Reproducible Analytical Pipelines with Nix' open_vignette: true check: true document: true overwrite: 'yes' -subshells.Rmd: - path: dev/subshells.Rmd +z-subshells.Rmd: + path: dev/z-subshells.Rmd state: active R: [] tests: [] vignettes: vignettes/z-advanced-topic-running-r-or-shell-code-in-nix-from-r.Rmd inflate: - flat_file: dev/subshells.Rmd + flat_file: dev/z-subshells.Rmd vignette_name: 'z - Advanced topic: Running R or Shell Code in Nix from R' open_vignette: true check: true diff --git a/dev/4a-install_r_pkgs.Rmd b/dev/d1-install_r_pkgs.Rmd similarity index 98% rename from dev/4a-install_r_pkgs.Rmd rename to dev/d1-install_r_pkgs.Rmd index 78946d44..b6c5b679 100644 --- a/dev/4a-install_r_pkgs.Rmd +++ b/dev/d1-install_r_pkgs.Rmd @@ -1,5 +1,5 @@ --- -title: "4a - Installing R packages in a Nix environment" +title: "d1 - Installing R packages in a Nix environment" output: html_document editor_options: chunk_output_type: console diff --git a/dev/4b-install_sys_pkgs.Rmd b/dev/d2-install_sys_pkgs.Rmd similarity index 98% rename from dev/4b-install_sys_pkgs.Rmd rename to dev/d2-install_sys_pkgs.Rmd index 420a0df0..88751374 100644 --- a/dev/4b-install_sys_pkgs.Rmd +++ b/dev/d2-install_sys_pkgs.Rmd @@ -1,5 +1,5 @@ --- -title: "4b - Installing system tools and TexLive packages in a Nix environment" +title: "d2 - Installing system tools and TexLive packages in a Nix environment" output: html_document editor_options: chunk_output_type: console diff --git a/dev/5-interactive_use.Rmd b/dev/e-interactive_use.Rmd similarity index 99% rename from dev/5-interactive_use.Rmd rename to dev/e-interactive_use.Rmd index 0d7c04f0..7abef0a4 100644 --- a/dev/5-interactive_use.Rmd +++ b/dev/e-interactive_use.Rmd @@ -1,5 +1,5 @@ --- -title: "5 - Interactive use" +title: "e - Interactive use" output: html_document editor_options: chunk_output_type: console diff --git a/dev/flat_available_R.Rmd b/dev/flat_available_R.Rmd new file mode 100644 index 00000000..627ffd7b --- /dev/null +++ b/dev/flat_available_R.Rmd @@ -0,0 +1,50 @@ +--- +title: "List available R versions" +output: html_document +editor_options: + chunk_output_type: console +--- + +```{r development, include=FALSE} +library(testthat) +``` + +This function returns available R versions: + +```{r function-available_r} +#' List available R versions from Nixpkgs +#' @return A character vector containing the available R versions. +#' @export +#' +#' @examples +#' available_r() +available_r <- function(){ + + temp <- new.env(parent = emptyenv()) + + data(list = "r_nix_revs", + package = "rix", + envir = temp) + + get("r_nix_revs", envir = temp) + + c("latest", r_nix_revs$version) +} + +``` + +```{r tests-available_r} +testthat::test_that("available_r lists all available r versions", { + testthat::expect_equal( + available_r(), + c("latest", "3.0.2", "3.0.3", "3.1.0", "3.1.2", "3.1.3", "3.2.0", "3.2.1", + "3.2.2", "3.2.3", "3.2.4", "3.3.3", "3.4.0", "3.4.1", "3.4.2", "3.4.3", + "3.4.4", "3.5.0", "3.5.1", "3.5.2", "3.5.3", "3.6.0", "3.6.1", "3.6.2", + "3.6.3", "4.0.0", "4.0.2", "4.0.3", "4.0.4", "4.1.1", "4.1.2", "4.1.3", + "4.2.0", "4.2.1", "4.2.2", "4.2.3", "4.3.1" + ) + ) +}) + + +``` diff --git a/dev/flat_build_envs.Rmd b/dev/flat_build_envs.Rmd deleted file mode 100644 index 7139eef8..00000000 --- a/dev/flat_build_envs.Rmd +++ /dev/null @@ -1,2199 +0,0 @@ ---- -title: "Functions to build environments" -output: html_document -editor_options: - chunk_output_type: console ---- - -```{r development, include=FALSE} -library(testthat) -``` - - - -```{r development-load} -# Load already included functions if relevant -pkgload::load_all(export_all = FALSE) -``` - -# Main functions - -This function takes an R version as an input and returns the Nix revision that provides it: - -```{r function-find_rev} -#' find_rev Find the right Nix revision -#' @param r_version Character. R version to look for, for example, "4.2.0". If a nixpkgs revision is provided instead, this gets returned. -#' @return A character. The Nix revision to use -#' -#' @examples -#' find_rev("4.2.0") -find_rev <- function(r_version) { - - stopifnot("r_version has to be a character." = is.character(r_version)) - - if(r_version == "latest"){ - return(get_latest()) - } else if(nchar(r_version) == 40){ - return(r_version) - } else { - - temp <- new.env(parent = emptyenv()) - - data(list = "r_nix_revs", - package = "rix", - envir = temp) - - get("r_nix_revs", envir = temp) - - output <- r_nix_revs$revision[r_nix_revs$version == r_version] - - stopifnot("Error: the provided R version is likely wrong. Please check that you provided a correct R version. You can list available versions using `available_r()`" = !identical(character(0), output)) - - output -} - -} - -``` - -```{r tests-find_rev} -testthat::test_that("find_rev returns correct nixpkgs hash", { - testthat::expect_equal( - find_rev("4.2.2"), - "8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8" - ) - - testthat::expect_equal( - find_rev("8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8"), - "8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8" - ) -}) -``` - -This function returns available R versions: - -```{r function-available_r} -#' List available R versions from Nixpkgs -#' @return A character vector containing the available R versions. -#' @export -#' -#' @examples -#' available_r() -available_r <- function(){ - - temp <- new.env(parent = emptyenv()) - - data(list = "r_nix_revs", - package = "rix", - envir = temp) - - get("r_nix_revs", envir = temp) - - c("latest", r_nix_revs$version) -} - -``` - -```{r tests-available_r} -testthat::test_that("available_r lists all available r versions", { - testthat::expect_equal( - available_r(), - c("latest", "3.0.2", "3.0.3", "3.1.0", "3.1.2", "3.1.3", "3.2.0", "3.2.1", - "3.2.2", "3.2.3", "3.2.4", "3.3.3", "3.4.0", "3.4.1", "3.4.2", "3.4.3", - "3.4.4", "3.5.0", "3.5.1", "3.5.2", "3.5.3", "3.6.0", "3.6.1", "3.6.2", - "3.6.3", "4.0.0", "4.0.2", "4.0.3", "4.0.4", "4.1.1", "4.1.2", "4.1.3", - "4.2.0", "4.2.1", "4.2.2", "4.2.3", "4.3.1" - ) - ) -}) - - -``` - -The function below will return the very latest commit from the unstable branch -of `NixOS/nixpkgs`. This will make sure that users that want to use the most -up-to-date version of R and R packages can do so: - -```{r function-get_latest} -#' get_latest Get the latest R version and packages -#' @return A character. The commit hash of the latest nixpkgs-unstable revision -#' @importFrom httr content GET stop_for_status -#' @importFrom jsonlite fromJSON -#' -#' @examples -get_latest <- function() { - api_url <- "https://api.github.com/repos/NixOS/nixpkgs/commits?sha=nixpkgs-unstable" - - tryCatch({ - response <- httr::GET(url = api_url) - httr::stop_for_status(response) - commit_data <- jsonlite::fromJSON(httr::content(response, "text")) - latest_commit <- commit_data$sha[1] - return(latest_commit) - }, error = function(e) { - cat("Error:", e$message, "\n") - return(NULL) - }) -} -``` - - -`get_sri_hash_deps()` returns the SRI hash of a NAR serialized path to a cloned -Github repository, or package source downloaded from the CRAN archives, alongside -that packages' build dependencies. These hashes are used by Nix for security purposes. In -order to get the hash, a GET to a service I've made gets made. This request -gets handled by a server with Nix installed, and so the SRI hash can get computed` -by `nix hash path --sri path_to_repo`. - -```{r function-get_sri_hash_deps} -#' get_sri_hash_deps Get the SRI hash of the NAR serialization of a Github repo -#' @param repo_url A character. The URL to the package's Github repository or to the `.tar.gz` package hosted on CRAN. -#' @param branch_name A character. The branch of interest, NULL for archived CRAN packages. -#' @param commit A character. The commit hash of interest, for reproducibility's sake, NULL for archived CRAN packages. -#' @importFrom httr content GET http_error -#' @return The SRI hash as a character -get_sri_hash_deps <- function(repo_url, branch_name, commit){ - result <- httr::GET(paste0("http://git2nixsha.dev:1506/hash?repo_url=", - repo_url, - "&branchName=", - branch_name, - "&commit=", - commit)) - - if(http_error(result)){ - stop(paste0("Error in pulling URL: ", repo_url, ". If it's a Github repo, check the url, branch name and commit. Are these correct? If it's an archived CRAN package, check the name of the package and the version number.")) - } - - - lapply(httr::content(result), unlist) - -} -``` - -```{r tests-get_sri_hash_deps} -testthat::test_that("get_sri_hash_deps returns correct sri hash and dependencies of R packages", { - testthat::expect_equal( - get_sri_hash_deps("https://github.com/rap4all/housing/", - "fusen", - "1c860959310b80e67c41f7bbdc3e84cef00df18e"), - list( - "sri_hash" = "sha256-s4KGtfKQ7hL0sfDhGb4BpBpspfefBN6hf+XlslqyEn4=", - "deps" = "dplyr ggplot2 janitor purrr readxl rlang rvest stringr tidyr" - ) - ) -}) - -testthat::test_that("Internet is out for fetchgit()", { - - testthat::local_mocked_bindings( - http_error = function(...) TRUE - ) - - expect_error( - get_sri_hash_deps( - "https://github.com/rap4all/housing/", - "fusen", - "1c860959310b80e67c41f7bbdc3e84cef00df18e" - ), - 'Error in pulling', - ) - -}) - -``` - -Function `fetchgit()` takes a git repository as an input and returns a -string using the Nix `fetchgit()` function to install the package. It -automatically finds the right `sha256` as well: - -```{r function-fetchgit} -#' fetchgit Downloads and installs a package hosted of Git -#' @param git_pkg A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. -#' @return A character. The Nix definition to download and build the R package from Github. -fetchgit <- function(git_pkg){ - - package_name <- git_pkg$package_name - repo_url <- git_pkg$repo_url - branch_name <- git_pkg$branch_name - commit <- git_pkg$commit - - output <- get_sri_hash_deps(repo_url, branch_name, commit) - sri_hash <- output$sri_hash - imports <- output$deps - - sprintf('(pkgs.rPackages.buildRPackage { - name = \"%s\"; - src = pkgs.fetchgit { - url = \"%s\"; - branchName = \"%s\"; - rev = \"%s\"; - sha256 = \"%s\"; - }; - propagatedBuildInputs = builtins.attrValues { - inherit (pkgs.rPackages) %s; - }; - })', - package_name, - repo_url, - branch_name, - commit, - sri_hash, - imports -) - -} - -``` - -```{r function-fetchzip} -#' fetchzip Downloads and installs an archived CRAN package -#' @param archive_pkg A character of the form "dplyr@0.80" -#' @return A character. The Nix definition to download and build the R package from CRAN. -fetchzip <- function(archive_pkg, sri_hash = NULL){ - - pkgs <- unlist(strsplit(archive_pkg, split = "@")) - - cran_archive_link <- paste0( - "https://cran.r-project.org/src/contrib/Archive/", - pkgs[1], "/", - paste0(pkgs[1], "_", pkgs[2]), - ".tar.gz") - - package_name <- pkgs[1] - repo_url <- cran_archive_link - - if(is.null(sri_hash)){ - output <- get_sri_hash_deps(repo_url, branch_name = NULL, commit = NULL) - sri_hash <- output$sri_hash - imports <- output$deps - } else { - sri_hash <- sri_hash - imports <- NULL - } - - sprintf('(pkgs.rPackages.buildRPackage { - name = \"%s\"; - src = pkgs.fetchzip { - url = \"%s\"; - sha256 = \"%s\"; - }; - propagatedBuildInputs = builtins.attrValues { - inherit (pkgs.rPackages) %s; - }; - })', - package_name, - repo_url, - sri_hash, - imports -) -} - - -``` - -This function is a wrapper around `fetchgit()` used to handle multiple Github -packages: - -```{r function-fetchgits} -#' fetchgits Downloads and installs a packages hosted of Git. Wraps `fetchgit()` to handle multiple packages -#' @param git_pkgs A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. This argument can also be a list of lists of these four elements. -#' @return A character. The Nix definition to download and build the R package from Github. -fetchgits <- function(git_pkgs){ - - if(!all(sapply(git_pkgs, is.list))){ - fetchgit(git_pkgs) - } else if(all(sapply(git_pkgs, is.list))){ - paste(lapply(git_pkgs, fetchgit), collapse = "\n") - } else { - stop("There is something wrong with the input. Make sure it is either a list of four elements 'package_name', 'repo_url', 'branch_name' and 'commit' or a list of lists with these four elements") - } - -} -``` - -```{r function-fetchzips} -#' fetchzips Downloads and installs packages hosted in the CRAN archives. Wraps `fetchzip()` to handle multiple packages. -#' @param archive_pkgs A character, or an atomic vector of characters. -#' @return A character. The Nix definition to download and build the R package from the CRAN archives. -fetchzips <- function(archive_pkgs){ - - if(is.null(archive_pkgs)){ - "" #Empty character in case the user doesn't need any packages from the CRAN archives. - } else if(length(archive_pkgs) == 1){ - fetchzip(archive_pkgs) - } else if(length(archive_pkgs) > 1){ - paste(lapply(archive_pkgs, fetchzip), collapse = "\n") - } else { - stop("There is something wrong with the input. Make sure it is either a sinle package name, or an atomic vector of package names, for example c('dplyr@0.8.0', 'tidyr@1.0.0').") - } - -} -``` - -```{r function-fetchpkgs} -#' fetchpkgs Downloads and installs packages hosted in the CRAN archives or Github. -#' @param git_pkgs A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. This argument can also be a list of lists of these four elements. -#' @param archive_pkgs A character, or an atomic vector of characters. -#' @return A character. The Nix definition to download and build the R package from the CRAN archives. -fetchpkgs <- function(git_pkgs, archive_pkgs){ - paste(fetchgits(git_pkgs), - fetchzips(archive_pkgs), - collapse = "\n") -} - -``` - -This next function returns a `default.nix` file that can be used to build a -reproducible environment. This function takes an R version as an input (the -correct Nix revision is found using `find_rev()`), a list of R packages, and -whether the user wants to work with RStudio or not in that environment. If -you use another IDE, you can leave the "ide" argument blank: - -```{r function-rix} -#' rix Generates a Nix expression that builds a reproducible development environment -#' @return Nothing, this function only has the side-effect of writing a file -#' called "default.nix" in the working directory. This file contains the -#' expression to build a reproducible environment using the Nix package -#' manager. -#' @param r_ver Character, defaults to "latest". The required R version, for example "4.0.0". -#' To use the latest version of R, use "latest", if you need the latest, bleeding edge version -#' of R and packages, then use "latest". You can check which R versions are available using `available_r`. -#' For reproducibility purposes, you can also provide a nixpkgs revision. -#' @param r_pkgs Vector of characters. List the required R packages for your -#' analysis here. -#' @param system_pkgs Vector of characters. List further software you wish to install that -#' are not R packages such as command line applications for example. -#' @param git_pkgs List. A list of packages to install from Git. See details for more information. -#' @param tex_pkgs Vector of characters. A set of tex packages to install. Use this if you need to compile `.tex` documents, or build PDF documents using Quarto. If you don't know which package to add, start by adding "amsmath". See the Vignette "Authoring LaTeX documents" for more details. -#' @param ide Character, defaults to "other". If you wish to use RStudio to work -#' interactively use "rstudio" or "code" for Visual Studio Code. For other editors, -#' use "other". This has been tested with RStudio, VS Code and Emacs. If other -#' editors don't work, please open an issue. -#' @param project_path Character, defaults to the current working directory. Where to write -#' `default.nix`, for example "/home/path/to/project". -#' The file will thus be written to the file "/home/path/to/project/default.nix". -#' @param overwrite Logical, defaults to FALSE. If TRUE, overwrite the `default.nix` -#' file in the specified path. -#' @param print Logical, defaults to FALSE. If TRUE, print `default.nix` to console. -#' @param shell_hook Character, defaults to `"R --vanilla"`. Commands added to the shell_hook get -#' executed when the Nix shell starts (via `shellHook`). So by default, using `nix-shell default.nix` will -#' start R. Set to NULL if you want bash to be started instead. -#' @details This function will write a `default.nix` in the chosen path. Using -#' the Nix package manager, it is then possible to build a reproducible -#' development environment using the `nix-build` command in the path. This -#' environment will contain the chosen version of R and packages, and will not -#' interfere with any other installed version (via Nix or not) on your -#' machine. Every dependency, including both R package dependencies but also -#' system dependencies like compilers will get installed as well in that -#' environment. If you use RStudio for interactive work, then set the -#' `rstudio` parameter to `TRUE`. If you use another IDE (for example Emacs or -#' Visual Studio Code), you do not need to add it to the `default.nix` file, -#' you can simply use the version that is installed on your computer. Once you built -#' the environment using `nix-build`, you can drop into an interactive session -#' using `nix-shell`. See the "Building reproducible development environments with rix" -#' vignette for detailled instructions. -#' Packages to install from Github must be provided in a list of 4 elements: -#' "package_name", "repo_url", "branch_name" and "commit". -#' This argument can also be a list of lists of these 4 elements. It is also possible to install old versions -#' of packages by specifying a version. For example, to install the latest -#' version of `{AER}` but an old version of `{ggplot2}`, you could -#' write: `r_pkgs = c("AER", "ggplot2@2.2.1")`. Note -#' however that doing this could result in dependency hell, because an older -#' version of a package might need older versions of its dependencies, but other -#' packages might need more recent versions of the same dependencies. If instead you -#' want to use an environment as it would have looked at the time of `{ggplot2}`'s -#' version 2.2.1 release, then use the Nix revision closest to that date, by setting -#' `r_ver = "3.1.0"`, which was the version of R current at the time. This -#' ensures that Nix builds a completely coherent environment. -#' By default, the nix shell will be configured with `"en_US.UTF-8"` for the -#' relevant locale variables (`LANG`, `LC_ALL`, `LC_TIME`, `LC_MONETARY`, -#' `LC_PAPER`, `LC_MEASUREMENT`). This is done to ensure locale -#' reproducibility by default in Nix environments created with `rix()`. -#' If there are good reasons to not stick to the default, you can set your -#' preferred locale variables via -#' `options(rix.nix_locale_variables = list(LANG = "de_CH.UTF-8", <...>)` -#' and the aforementioned locale variable names. -#' @export -#' @examples -#' \dontrun{ -#' # Build an environment with the latest version of R -#' # and the dplyr and ggplot2 packages -#' rix(r_ver = "latest", -#' r_pkgs = c("dplyr", "ggplot2"), -#' system_pkgs = NULL, -#' git_pkgs = NULL, -#' ide = "code", -#' project_path = path_default_nix, -#' overwrite = TRUE, -#' print = TRUE) -#' } -rix <- function(r_ver = "latest", - r_pkgs = NULL, - system_pkgs = NULL, - git_pkgs = NULL, - tex_pkgs = NULL, - ide = "other", - project_path = ".", - overwrite = FALSE, - print = FALSE, - shell_hook = "R --vanilla"){ - - stopifnot("'ide' has to be one of 'other', 'rstudio' or 'code'" = (ide %in% c("other", "rstudio", "code"))) - - project_path <- if(project_path == "."){ - "default.nix" - } else { - paste0(project_path, "/default.nix") - } - - # Generate the correct text for the header depending on wether - # an R version or a Nix revision is supplied to `r_ver` - if(nchar(r_ver) > 20){ - r_ver_text <- paste0("as it was as of nixpkgs revision: ", r_ver) - } else { - r_ver_text <- paste0("version ", r_ver) - } - - # Find the Nix revision to use - nix_revision <- find_rev(r_ver) - - project_path <- file.path(project_path) - - rix_call <- match.call() - - generate_rix_call <- function(rix_call, nix_revision){ - - rix_call$r_ver <- nix_revision - - rix_call <- paste0("# >", deparse1(rix_call)) - - gsub(",", ",\n# >", rix_call) - } - - # Get the rix version - rix_version <- utils::packageVersion("rix") - - generate_header <- function(rix_version, - nix_revision, - r_ver_text, - rix_call){ - - if(identical(Sys.getenv("TESTTHAT"), "true")){ - sprintf(' -let - pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/%s.tar.gz") {}; -', -nix_revision) - } else { - sprintf('# This file was generated by the {rix} R package v%s on %s -# with following call: -%s -# It uses nixpkgs\' revision %s for reproducibility purposes -# which will install R %s -# Report any issues to https://github.com/b-rodrigues/rix -let - pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/%s.tar.gz") {}; -', -rix_version, -Sys.Date(), -generate_rix_call(rix_call, nix_revision), -nix_revision, -r_ver_text, -nix_revision -) - } - - } - - # Now we need to generate all the different sets of packages - # to install. Let's start by the CRAN packages, current - # and archived. The function below builds the strings. - get_rPackages <- function(r_pkgs){ - - # in case users pass something like c("dplyr", "tidyr@1.0.0") - # r_pkgs will be "dplyr" only - # and "tidyr@1.0.0" needs to be handled by fetchzips - r_and_archive_pkgs <- detect_versions(r_pkgs) - - # overwrite r_pkgs - r_pkgs <- r_and_archive_pkgs$cran_packages - - # get archive_pkgs - archive_pkgs <- r_and_archive_pkgs$archive_packages - - r_pkgs <- if(ide == "code"){ - c(r_pkgs, "languageserver") - } else { - r_pkgs - } - - rPackages <- paste(r_pkgs, collapse = ' ') - - rPackages <- gsub('\\.', '_', rPackages) - - list("rPackages" = rPackages, - "archive_pkgs" = archive_pkgs) - - } - - # Get the two lists. One list is current CRAN packages - # the other is archived CRAN packages. - cran_pkgs <- get_rPackages(r_pkgs) - - # we need to know if the user wants R packages - - flag_rpkgs <- if(is.null(cran_pkgs$rPackages) | cran_pkgs$rPackages == ""){ - "" - } else { - "rpkgs" - } - - # generate_* function generate the actual Nix code - generate_rpkgs <- function(rPackages) { - if (flag_rpkgs == ""){ - NULL - } else { - sprintf('rpkgs = builtins.attrValues { - inherit (pkgs.rPackages) %s; -}; -', -rPackages) - } - } - - # Texlive packages - generate_tex_pkgs <- function(tex_pkgs) { - if (!is.null(tex_pkgs)) { - - tex_pkgs <- paste(tex_pkgs, collapse = ' ') - - sprintf('tex = (pkgs.texlive.combine { - inherit (pkgs.texlive) scheme-small %s; -}); -', -tex_pkgs) - } - } - - flag_tex_pkgs <- if(is.null(tex_pkgs)){ - "" - } else { - "tex" - } - - # system packages - get_system_pkgs <- function(system_pkgs, r_pkgs){ - - system_pkgs <- if(any(grepl("quarto", r_pkgs))){ - unique(c(system_pkgs, "quarto")) - } else { - system_pkgs - } - - paste(system_pkgs, collapse = ' ') - } - - flag_git_archive <- if(!is.null(cran_pkgs$archive) | !is.null(git_pkgs)){ - "git_archive_pkgs" - } else { - "" - } - - generate_git_archived_packages <- function(git_pkgs, archive_pkgs){ - if(flag_git_archive == ""){ - NULL - } else { - sprintf('git_archive_pkgs = [%s];\n', - fetchpkgs(git_pkgs, archive_pkgs) - ) - } - } - - - # `R` needs to be added. If we were using the rWrapper - # this wouldn't be needed, but we're not so we need - # to add it. - generate_system_pkgs <- function(system_pkgs, r_pkgs){ - sprintf('system_packages = builtins.attrValues { - inherit (pkgs) R glibcLocales nix %s; -}; -', -get_system_pkgs(system_pkgs, r_pkgs)) - } - - generate_locale_variables <- function() { - locale_defaults <- list( - LANG = "en_US.UTF-8", - LC_ALL = "en_US.UTF-8", - LC_TIME = "en_US.UTF-8", - LC_MONETARY = "en_US.UTF-8", - LC_PAPER = "en_US.UTF-8", - LC_MEASUREMENT = "en_US.UTF-8" - ) - locale_variables <- getOption( - "rix.nix_locale_variables", - default = locale_defaults - ) - valid_vars <- all(names(locale_variables) %in% names(locale_defaults)) - if (!isTRUE(valid_vars)) { - stop("`options(rix.nix_locale_variables = list())` ", - "only allows the following element names (locale variables):\n", - paste(names(locale_defaults), collapse = "; "), - call. = FALSE) - } - locale_vars <- paste( - Map(function(x, nm) paste0(nm, ' = ', '"', x, '"'), - nm = names(locale_variables), x = locale_variables), - collapse = ";\n " - ) - paste0(locale_vars, ";\n") - } - - generate_rstudio_pkgs <- function(ide, flag_git_archive, flag_rpkgs){ - if(ide == "rstudio"){ - sprintf('rstudio_pkgs = pkgs.rstudioWrapper.override { - packages = [ %s %s ]; -}; -', -flag_git_archive, -flag_rpkgs -) - } else { - NULL - } - } - - flag_rstudio <- if (ide == "rstudio") "rstudio_pkgs" else "" - - shell_hook <- if (!is.null(shell_hook) && nzchar(shell_hook)) { - paste0('shellHook = "', shell_hook, '";') - } else {''} - - # Generate the shell - generate_shell <- function(flag_git_archive, - flag_rpkgs){ - sprintf('in - pkgs.mkShell { - %s - %s - buildInputs = [ %s %s %s system_packages %s ]; - %s - }', - generate_locale_archive(detect_os()), - generate_locale_variables(), - flag_git_archive, - flag_rpkgs, - flag_tex_pkgs, - flag_rstudio, - shell_hook - ) - - } - - # Generate default.nix file - default.nix <- paste( - generate_header(rix_version, - nix_revision, - r_ver_text, - rix_call), - generate_rpkgs(cran_pkgs$rPackages), - generate_git_archived_packages(git_pkgs, cran_pkgs$archive_pkgs), - generate_tex_pkgs(tex_pkgs), - generate_system_pkgs(system_pkgs, r_pkgs), - generate_rstudio_pkgs(ide, flag_git_archive, flag_rpkgs), - generate_shell(flag_git_archive, flag_rpkgs), - collapse = "\n" - ) - - default.nix <- readLines(textConnection(default.nix)) - - if(print){ - cat(default.nix, sep = "\n") - } - - if(!file.exists(project_path) || overwrite){ - writeLines(default.nix, project_path) - } else { - stop(paste0("File exists at ", project_path, ". Set `overwrite == TRUE` to overwrite.")) - } - - - -} -``` - -```{r, tests-rix} -testthat::test_that("Snapshot test of rix()", { - - save_default_nix_test <- function(ide) { - - path_default_nix <- tempdir() - - rix(r_ver = "4.3.1", - r_pkgs = c("dplyr", "janitor", "AER@1.2-8", "quarto"), - tex_pkgs = c("amsmath"), - git_pkgs = list( - list(package_name = "housing", - repo_url = "https://github.com/rap4all/housing/", - branch_name = "fusen", - commit = "1c860959310b80e67c41f7bbdc3e84cef00df18e"), - list(package_name = "fusen", - repo_url = "https://github.com/ThinkR-open/fusen", - branch_name = "main", - commit = "d617172447d2947efb20ad6a4463742b8a5d79dc") - ), - ide = ide, - project_path = path_default_nix, - overwrite = TRUE) - - paste0(path_default_nix, "/default.nix") - - } - - testthat::announce_snapshot_file("find_rev/rstudio_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(ide = "rstudio"), - name = "rstudio_default.nix", - ) - - testthat::announce_snapshot_file("find_rev/other_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(ide = "other"), - name = "other_default.nix" - ) - - testthat::announce_snapshot_file("find_rev/code_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(ide = "code"), - name = "code_default.nix" - ) - -}) - -``` - -```{r, tests-add_quarto_to_sys_pkgs} -testthat::test_that("Quarto gets added to sys packages", { - - save_default_nix_test <- function(pkgs) { - - path_default_nix <- tempdir() - - rix(r_ver = "4.3.1", - r_pkgs = pkgs, - ide = "other", - project_path = path_default_nix, - overwrite = TRUE) - - paste0(path_default_nix, "/default.nix") - - } - - testthat::announce_snapshot_file("find_rev/no_quarto_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(pkgs = "dplyr"), - name = "no_quarto_default.nix", - ) - - testthat::announce_snapshot_file("find_rev/yes_quarto_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(pkgs = c("dplyr", "quarto")), - name = "yes_quarto_default.nix" - ) - -}) - -``` - - -This is a helper to create `./inst/extadata/default.nix`. - -```{r, function-create_default_nix} -#' @noRd -create_default_nix <- function(path = file.path("inst", "extdata", - "default.nix")) { - if (!dir.exists(dirname(path))) { - stop("Path", path, " does not exist.") - } - - rix( - r_ver = "latest", - r_pkgs = NULL, - system_pkgs = NULL, - git_pkgs = list( - list( - package_name = "rix", - repo_url = "https://github.com/b-rodrigues/rix", - branch_name = "master", - commit = "22711ee98c0092e56c620122800ca8f30b773a65" - ) - ), - ide = "other", - project_path = dirname(path), - overwrite = TRUE, - shell_hook = "R --vanilla" - ) -} -``` - - -The function below is to invoke the shell command `nix-build` from a R -session. - -```{r function-nix_build} -#' Invoke shell command `nix-build` from an R session -#' @param project_path Path to the folder where the `default.nix` file resides. -#' The default is `"."`, which is the working directory in the current R -#' session. -#' @param exec_mode Either `"blocking"` (default) or `"non-blocking`. This -#' will either block the R session while the `nix-build` shell command is -#' executed, or run `nix-build` in the background ("non-blocking"). -#' @return integer of the process ID (PID) of `nix-build` shell command -#' launched, if `nix_build()` call is assigned to an R object. Otherwise, it -#' will be returned invisibly. -#' @details The `nix-build` command line interface has more arguments. We will -#' probably not support all of them in this R wrapper, but currently we have -#' support for the following `nix-build` flags: -#' - `--max-jobs`: Maximum number of build jobs done in parallel by Nix. -#' According to the official docs of Nix, it defaults to `1`, which is one -#' core. This option can be useful for shared memory multiprocessing or -#' systems with high I/O latency. To set `--max-jobs` used, you can declare -#' with `options(rix.nix_build_max_jobs = )`. Once you call -#' `nix_build()` the flag will be propagated to the call of `nix-build`. -#' @export -#' @examples -#' \dontrun{ -#' nix_build() -#' } -nix_build <- function(project_path = ".", - exec_mode = c("blocking", "non-blocking")) { - has_nix_build <- nix_build_installed() # TRUE if yes, FALSE if no - nix_file <- file.path(project_path, "default.nix") - - stopifnot( - "`project_path` must be character of length 1." = - is.character(project_path) && length(project_path) == 1L, - "`project_path` has no `default.nix` file. Use one that contains `default.nix`" = - file.exists(nix_file), - "`nix-build` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = - isTRUE(has_nix_build) - ) - exec_mode <- match.arg(exec_mode) - - max_jobs <- getOption("rix.nix_build_max_jobs", default = 1L) - stopifnot("option `rix.nix_build_max_jobs` is not integerish" = - is_integerish(max_jobs)) - max_jobs <- as.integer(max_jobs) - - if (max_jobs == 1L) { - cmd <- c("nix-build", nix_file) - } else { - cmd <- c("nix-build", "--max-jobs", as.character(max_jobs), nix_file) - } - - cat(paste0("Launching `", paste0(cmd, collapse = " "), "`", " in ", - exec_mode, " mode\n")) - - proc <- switch(exec_mode, - "blocking" = sys::exec_internal(cmd = cmd), - "non-blocking" = sys::exec_background(cmd = cmd), - stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') - ) - - if (exec_mode == "non-blocking") { - poll_sys_proc_nonblocking(cmd, proc, what = "nix-build") - } else if (exec_mode == "blocking") { - poll_sys_proc_blocking(cmd, proc, what = "nix-build") - } - - # todo (?): clean zombies for background/non-blocking mode - - return(invisible(proc)) -} - -#' @noRd -poll_sys_proc_blocking <- function(cmd, proc, - what = c("nix-build", "expr")) { - what <- match.arg(what) - status <- proc$status - if (status == 0L) { - cat(paste0("\n==> ", sys::as_text(proc$stdout))) - cat(paste0("\n==> `", what, "` succeeded!")) - } else { - msg <- nix_build_exit_msg() - cat(paste0("`", cmd, "`", " failed with ", msg)) - } - return(invisible(status)) -} - -#' @noRd -poll_sys_proc_nonblocking <- function(cmd, proc, - what = c("nix-build", "expr")) { - what <- match.arg(what) - cat(paste0("\n==> Process ID (PID) is ", proc, ".")) - cat("\n==> Receiving stdout and stderr streams...\n") - status <- sys::exec_status(proc, wait = TRUE) - if (status == 0L) { - cat(paste0("\n==> `", what, "` succeeded!")) - } - return(invisible(status)) -} - -#' @noRd -is_integerish <- function(x, tol = .Machine$double.eps^0.5) { - return(abs(x - round(x)) < tol) -} - -#' @noRd -nix_build_installed <- function() { - exit_code <- system2("command", "-v", "nix-build") - if (exit_code == 0L) { - return(invisible(TRUE)) - } else { - return(invisible(FALSE)) - } -} - -#' @noRd -nix_build_exit_msg <- function(x) { - x_char <- as.character(x) - - err_msg <- switch( - x_char, - "100" = "generic build failure (100).", - "101" = "build timeout (101).", - "102" = "hash mismatch (102).", - "104" = "not deterministic (104).", - stop(paste0("general exit code ", x_char, ".")) - ) - - return(err_msg) -} -``` - -This function bootstraps and maintains an isolated, project-specific R setup -via Nix - -```{r, function-rix_init} -#' Initiate and maintain an isolated, project-specific, and runtime-pure R -#' setup via Nix. -#' -#' Creates an isolated project folder for a Nix-R configuration. `rix::rix_init()` -#' also adds, appends, or updates with or without backup a custom `.Rprofile` -#' file with code that initializes a startup R environment without system's user -#' libraries within a Nix software environment. Instead, it restricts search -#' paths to load R packages exclusively from the Nix store. Additionally, it -#' makes Nix utilities like `nix-shell` available to run system commands from -#' the system's RStudio R session, for both Linux and macOS. -#' -#' **Enhancement of computational reproducibility for Nix-R environments:** -#' -#' The primary goal of `rix::rix_init()` is to enhance the computational -#' reproducibility of Nix-R environments during runtime. Notably, no restart is -#' required as environmental variables are set in the current session, in -#' addition to writing an `.Rprofile` file. This is particularly useful to make -#' [rix::with_nix()] evaluate custom R functions from any "Nix-to-Nix" or -#' "System-to-Nix" R setups. It introduces two side-effects that -#' take effect both in a current or later R session setup: -#' -#' 1. **Adjusting `R_LIBS_USER` path:** -#' By default, the first path of `R_LIBS_USER` points to the user library -#' outside the Nix store (see also [base::.libPaths()]). This creates -#' friction and potential impurity as R packages from the system's R user -#' library are loaded. While this feature can be useful for interactively -#' testing an R package in a Nix environment before adding it to a `.nix` -#' configuration, it can have undesired effects if not managed carefully. -#' A major drawback is that all R packages in the `R_LIBS_USER` location need -#' to be cleaned to avoid loading packages outside the Nix configuration. -#' Issues, especially on macOS, may arise due to segmentation faults or -#' incompatible linked system libraries. These problems can also occur -#' if one of the (reverse) dependencies of an R package is loaded along the -#' process. -#' -#' 2. **Make Nix commands available when running system commands from RStudio:** -#' In a host RStudio session not launched via Nix (`nix-shell`), the -#' environmental variables from `~/.zshrc` or `~/.bashrc` may not be -#' inherited. Consequently, Nix command line interfaces like `nix-shell` -#' might not be found. The `.Rprofile` code written by `rix::rix_init()` ensures -#' that Nix command line programs are accessible by adding the path of the -#' "bin" directory of the default Nix profile, -#' `"/nix/var/nix/profiles/default/bin"`, to the `PATH` variable in an -#' RStudio R session. -#' -#' These side effects are particularly recommended when working in flexible R -#' environments, especially for users who want to maintain both the system's -#' native R setup and utilize Nix expressions for reproducible development -#' environments. This init configuration is considered pivotal to enhance the -#' adoption of Nix in the R community, particularly until RStudio in Nixpkgs is -#' packaged for macOS. We recommend calling `rix::rix_init()` prior to comparing R -#' code ran between two software environments with `rix::with_nix()`. -#' -#' @param project_path Character with the folder path to the isolated nix-R project. -#' Defaults to `"."`, which is the current working directory path. If the folder -#' does not exist yet, it will be created. -#' @param rprofile_action Character. Action to take with `.Rprofile` file -#' destined for `project_path` folder. Possible values include -#' `"create_missing"`, which only writes `.Rprofile` if it -#' does not yet exist (otherwise does nothing); `"create_backup"`, which copies -#' the existing `.Rprofile` to a new backup file, generating names with -#' POSIXct-derived strings that include the time zone information. A new -#' `.Rprofile` file will be written with default code from `rix::rix_init()`; -#' `"overwrite"` overwrites the `.Rprofile` file if it does exist; `"append"` -#' appends the existing file with code that is tailored to an isolated Nix-R -#' project setup. -#' @param message_type Character. Message type, defaults to `"simple"`, which -#' gives minimal but sufficient feedback. Other values are currently -#' `"verbose"`, which provides more detailed diagnostics. -#' @export -#' @seealso [with_nix()] -#' @return Nothing, this function only has the side-effect of writing a file -#' called ".Rprofile" to the specified path. -#' @examples -#' \dontrun{ -#' # create an isolated, runtime-pure R setup via Nix -#' project_path <- "./sub_shell" -#' rix_init( -#' project_path = project_path, -#' rprofile_action = "create_missing" -#' ) -#' } -rix_init <- function(project_path = ".", - rprofile_action = c("create_missing", "create_backup", - "overwrite", "append"), - message_type = c("simple", "verbose")) { - message_type <- match.arg(message_type, choices = c("simple", "verbose")) - rprofile_action <- match.arg(rprofile_action, - choices = c("create_missing", "create_backup", "overwrite", "append")) - stopifnot( - "`project_path` needs to be character of length 1" = - is.character(project_path) && length(project_path) == 1L - ) - - cat("\n### Bootstrapping isolated, project-specific, and runtime-pure", - "R setup via Nix ###\n\n") - if (isFALSE(dir.exists(project_path))) { - dir.create(path = project_path, recursive = TRUE) - project_path <- normalizePath(path = project_path) - cat("==> Created isolated nix-R project folder:\n", project_path, "\n") - } else { - project_path <- normalizePath(path = project_path) - cat("==> Existing isolated nix-R project folder:\n", project_path, - "\n") - } - - # create project-local `.Rprofile` with pure settings - # first create the call, deparse it, and write it to .Rprofile - rprofile_quoted <- nix_rprofile() - rprofile_deparsed <- deparse_chr1(expr = rprofile_quoted, collapse = "\n") - rprofile_file <- file.path(project_path, ".Rprofile") - - rprofile_text <- get_rprofile_text(rprofile_deparsed) - write_rprofile <- function(rprofile_text, rprofile_file) { - writeLines( - text = rprofile_text, - con = file(rprofile_file) - ) - } - - is_nixr <- is_nix_rsession() - is_rstudio <- is_rstudio_session() - - rprofile_exists <- file.exists(rprofile_file) - timestamp <- format(Sys.time(), "%Y-%m-%dT%H:%M:%S%z") - rprofile_backup <- paste0(rprofile_file, "_backup_", timestamp) - - switch(rprofile_action, - create_missing = { - if (isTRUE(rprofile_exists)) { - cat( - "\n* Keep existing `.Rprofile`. in `project_path`:\n", - paste0(project_path, "/"), "\n" - ) - } else { - write_rprofile(rprofile_text, rprofile_file) - message_rprofile(action_string = "Added", project_path = project_path) - } - set_message_session_PATH(message_type = message_type) - }, - create_backup = { - if (isTRUE(rprofile_exists)) { - file.copy(from = rprofile_file, to = rprofile_backup) - cat( - "\n==> Backed up existing `.Rprofile` in file:\n", rprofile_backup, - "\n" - ) - write_rprofile(rprofile_text, rprofile_file) - message_rprofile( - action_string = "Overwrote", - project_path = project_path - ) - if (message_type == "verbose") { - cat("\n* Current lines of local `.Rprofile` are\n:") - cat(readLines(con = file(rprofile_file)), sep = "\n") - } - set_message_session_PATH(message_type = message_type) - } - }, - overwrite = { - write_rprofile(rprofile_text, rprofile_file) - if (isTRUE(rprofile_exists)) { - message_rprofile( - action_string = "Overwrote", project_path = project_path - ) - } else { - message_rprofile( - action_string = "Added", project_path = project_path - ) - } - }, - append = { - cat(paste0(rprofile_text, "\n"), file = rprofile_file, append = TRUE) - message_rprofile( - action_string = "Appended", project_path = project_path - ) - } - ) - - if (message_type == "verbose") { - cat("\n* Current lines of local `.Rprofile` are:\n\n") - cat(readLines(con = file(rprofile_file)), sep = "\n") - } - - on.exit(close(file(rprofile_file))) -} - -#' @noRd -get_rprofile_text <- function(rprofile_deparsed) { - c( -"### File generated by `rix::rix_init()` ### -# 1. Currently, system RStudio does not inherit environmental variables -# defined in `$HOME/.zshrc`, `$HOME/.bashrc` and alike. This is workaround to -# make the path of the nix store and hence basic nix commands available -# in an RStudio session -# 2. For nix-R session, remove `R_LIBS_USER`, system's R user library.`. -# This guarantees no user libraries from the system are loaded and only -# R packages in the Nix store are used. This makes Nix-R behave in pure manner -# at run-time.", - rprofile_deparsed - ) -} - -#' @noRd -message_rprofile <- function(action_string = "Added", - project_path = ".") { - msg <- paste0( - "\n==> ", action_string, - " `.Rprofile` file and code lines for new R sessions launched from:\n", - project_path, - "\n\n* Added the location of the Nix store to `PATH` ", - "environmental variable for new R sessions on host/docker RStudio:\n", - "/nix/var/nix/profiles/default/bin" - ) - cat(msg) -} - -#' @noRd -set_message_session_PATH <- function(message_type = c("simple", "verbose")) { - match.arg(message_type, choices = c("simple", "verbose")) - if (message_type == "verbose") { - cat("\n\n* Current `PATH` variable set in R session is:\n\n") - cat(Sys.getenv("PATH")) - } - cat("\n\n==> Also adjusting `PATH` via `Sys.setenv()`, so that", - "system commands can invoke key Nix commands like `nix-build` in this", - "RStudio session on the host operating system.") - PATH <- set_nix_path() - if (message_type == "verbose") { - cat("\n\n* Updated `PATH` variable is:\n\n", PATH) - } -} - -#' @noRd -is_nix_rsession <- function() { - is_nixr <- nzchar(Sys.getenv("NIX_STORE")) - if (isTRUE(is_nixr)) { - cat("==> R session running via Nix (nixpkgs)\n") - return(TRUE) - } else { - cat("\n==> R session running via host operating system or docker\n") - return(FALSE) - } -} - -#' @noRd -is_rstudio_session <- function() { - is_rstudio <- Sys.getenv("RSTUDIO") == "1" - if (isTRUE(is_rstudio)) { - cat("\n==> R session running from RStudio\n") - return(TRUE) - } else { - cat("* R session not running from RStudio") - return(FALSE) - } -} - -#' @noRd -set_nix_path <- function() { - old_path <- Sys.getenv("PATH") - nix_path <- "/nix/var/nix/profiles/default/bin" - has_nix_path <- any(grepl(nix_path, old_path)) - if (isFALSE(has_nix_path)) { - Sys.setenv( - PATH = paste(old_path, "/nix/var/nix/profiles/default/bin", sep = ":") - ) - } - invisible(Sys.getenv("PATH")) -} - -#' @noRd -nix_rprofile <- function() { - quote( { - is_rstudio <- Sys.getenv("RSTUDIO") == "1" - is_nixr <- nzchar(Sys.getenv("NIX_STORE")) - if (isFALSE(is_nixr) && isTRUE(is_rstudio)) { - # Currently, RStudio does not propagate environmental variables defined in - # `$HOME/.zshrc`, `$HOME/.bashrc` and alike. This is workaround to - # make the path of the nix store and hence basic nix commands available - # in an RStudio session - cat("{rix} detected RStudio R session") - old_path <- Sys.getenv("PATH") - nix_path <- "/nix/var/nix/profiles/default/bin" - has_nix_path <- any(grepl(nix_path, old_path)) - if (isFALSE(has_nix_path)) { - Sys.setenv( - PATH = paste( - old_path, nix_path, sep = ":" - ) - ) - } - rm(old_path, nix_path) - } - - if (isTRUE(is_nixr)) { - current_paths <- .libPaths() - userlib_paths <- Sys.getenv("R_LIBS_USER") - user_dir <- grep(paste(userlib_paths, collapse = "|"), current_paths) - new_paths <- current_paths[-user_dir] - # sets new library path without user library, making nix-R pure at - # run-time - .libPaths(new_paths) - rm(current_paths, userlib_paths, user_dir, new_paths) - } - - rm(is_rstudio, is_nixr) - } ) -} - -``` - -```{r, tests-rix_init} -testthat::test_that("Snapshot test of rix_init()", { - - #skip_on_covr() - - save_rix_init_test <- function() { - - path_env_nix <- tempdir() - - rix_init( - project_path = path_env_nix, - rprofile_action = "overwrite", - message_type = "simple" - ) - - paste0(path_env_nix, "/.Rprofile") - - } - - testthat::announce_snapshot_file("find_rev/golden_Rprofile.txt") - - testthat::expect_snapshot_file( - path = save_rix_init_test(), - name = "golden_Rprofile.txt", - ) -}) -``` - -This function can evaluate an shell or R expression in Nix via `nix-shell` -environment. - -```{r, function-with-nix} - -#' Evaluate function in R or shell command via `nix-shell` environment -#' -#' This function needs an installation of Nix. `with_nix()` has two effects -#' to run code in isolated and reproducible environments. -#' 1. Evaluate a function in R or a shell command via the `nix-shell` -#' environment (Nix expression for custom software libraries; involving pinned -#' versions of R and R packages via Nixpkgs) -#' 2. If no error, return the result object of `expr` in `with_nix()` into the -#' current R session. -#' -#' -#' -#' `with_nix()` gives you the power of evaluating a main function `expr` -#' and its function call stack that are defined in the current R session -#' in an encapsulated nix-R session defined by Nix expression (`default.nix`), -#' which is located in at a distinct project path (`project_path`). -#' -#' `with_nix()` is very convenient because it gives direct code feedback in -#' read-eval-print-loop style, which gives a direct interface to the very -#' reproducible infrastructure-as-code approach offered by Nix and Nixpkgs. You -#' don't need extra efforts such as setting up DevOps tooling like Docker and -#' domain specific tools like {renv} to control complex software environments in -#' R and any other language. It is for example useful for the following -#' purposes. -#' -#' 1. test compatibility of custom R code and software/package dependencies in -#' development and production environments -#' 2. directly stream outputs (returned objects), messages and errors from any -#' command line tool offered in Nixpkgs into an R session. -#' 3. Test if evolving R packages change their behavior for given unchanged -#' R code, and whether they give identical results or not. -#' -#' #' `with_nix()` can evaluate both R code from a nix-R session within -#' another nix-R session, and also from a host R session (i.e., on macOS or -#' Linux) within a specific nix-R session. This feature is useful for testing -#' the reproducibility and compatibility of given code across different software -#' environments. If testing of different sets of environments is necessary, you -#' can easily do so by providing Nix expressions in custom `.nix` or -#' `default.nix` files in different subfolders of the project. -#' -#' To do its job, `with_nix()` heavily relies on patterns that manipulate -#' language expressions (aka computing on the language) offered in base R as -#' well as the {codetools} package by Luke Tierney. -#' -#' Some of the key steps that are done behind the scene: -#' 1. recursively find, classify, and export global objects (globals) in the -#' call stack of `expr` as well as propagate R package environments found. -#' 2. Serialize (save to disk) and deserialize (read from disk) dependent -#' data structures as `.Rds` with necessary function arguments provided, -#' any relevant globals in the call stack, packages, and `expr` outputs -#' returned in a temporary directory. -#' 3. Use pure `nix-shell` environments to execute a R code script -#' reconstructed catching expressions with quoting; it is launched by commands -#' like this via `{sys}` by Jeroen Ooms: -#' `nix-shell --pure --run "Rscript --vanilla"`. -#' -#' @param expr Single R function or call, or character vector of length one with -#' shell command and possibly options (flags) of the command to be invoked. -#' For `program = R`, you can both use a named or an anonymous function. -#' The function provided in `expr` should not evaluate when you pass arguments, -#' hence you need to wrap your function call like -#' `function() your_fun(arg_a = "a", arg_b = "b")`, to avoid evaluation and make -#' sure `expr` is a function (see details and examples). -#' @param program String stating where to evaluate the expression. Either `"R"`, -#' the default, or `"shell"`. `where = "R"` will evaluate the expression via -#' `RScript` and `where = "shell"` will run the system command in `nix-shell`. -#' @param exec_mode Either `"blocking"` (default) or `"non-blocking`. This -#' will either block the R session while `expr` is running in a `nix-shell` -#' environment, oor running it in the background ("non-blocking"). While -#' `program = R` will yield identical results for foreground and background -#' evaluation (R object), `program = "shell"` will return list of exit status, -#' standard output and standard error of the system command and as text in -#' blocking mode. -#' @param project_path Path to the folder where the `default.nix` file resides. -#' The default is `"."`, which is the working directory in the current R -#' session. This approach also useful when you have different subfolders -#' with separate software environments defined in different `default.nix` files. -#' If you prefer to run code in custom `.nix` files in the same directory -#' using `with_nix()`, you can use the `nix_file` argument to specify paths -#' to `.nix` files. -#' @param nix_file Path to `.nix` file that contains the expressions defining -#' the Nix software environment in which you want to run `expr`. See -#' `project_path` argument as an alternative way to specify the environment. -#' @param message_type String how detailed output is. Currently, there is -#' either `"simple"` (default) or `"verbose"`, which shows the script that runs -#' via `nix-shell`. -#' @importFrom codetools findGlobals checkUsage -#' @export -#' @return -#' - if `program = "R"`, R object returned by function given in `expr` -#' when evaluated via the R environment in `nix-shell` defined by Nix -#' expression. -#' - if `program = "shell"`, list with the following elements: -#' - `status`: exit code -#' - `stdout`: character vector with standard output -#' - `stderr`: character vector with standard error -#' of `expr` command sent to a command line interface provided by a Nix package. -#' @examples -#' \dontrun{ -#' # create an isolated, runtime-pure R setup via Nix -#' project_path <- "./sub_shell" -#' rix_init( -#' project_path = project_path, -#' rprofile_action = "create_missing" -#' ) -#' # generate nix environment in `default.nix` -#' rix( -#' r_ver = "4.2.0", -#' project_path = project_path -#' ) -#' # evaluate function in Nix-R environment via `nix-shell` and `Rscript`, -#' # stream messages, and bring output back to current R session -#' out <- with_nix( -#' expr = function(mtcars) nrow(mtcars), -#' program = "R", exec_mode = "non-blocking", project_path = project_path, -#' message_type = "simple" -#' ) -#' -#' # There no limit in the complexity of function call stacks that `with_nix()` -#' # can possibly handle; however, `expr` should not evaluate and -#' # needs to be a function for `program = "R"`. If you want to pass the -#' # a function with arguments, you can do like this -#' get_sample <- function(seed, n) { -#' set.seed(seed) -#' out <- sample(seq(1, 10), n) -#' return(out) -#' } -#' -#' out <- with_nix( -#' expr = get_sample(seed = 1234, n = 5), -#' program = "R", exec_mode = "non-blocking", -#' project_path = ".", -#' message_type = "simple" -#' ) -#' -#' #' ## You can also use packages, which will be exported to the nix-R session -#' ## running through `nix-shell` environment -#' R 4.2.2 -#' } -with_nix <- function(expr, - program = c("R", "shell"), - exec_mode = c("blocking", "non-blocking"), - project_path = ".", - nix_file = NULL, - message_type = c("simple", "verbose")) { - if (is.null(nix_file)) { - nix_file <- file.path(project_path, "default.nix") - } - stopifnot( - "`project_path` must be character of length 1." = - is.character(project_path) && length(project_path) == 1L, - "`project_path` has no `default.nix` file. Use one that contains `default.nix`" = - file.exists(nix_file), - "`message_type` must be character." = is.character(message_type), - "`expr` needs to be a call or function for `program = R`, and character of length 1 for `program = shell`" = - is.function(expr) || is.call(expr) || (is.character(expr) && length(expr) == 1L) - ) - - # ad-hoc solution for RStudio's limitation that R sessions cannot yet inherit - # proper `PATH` from custom `.Rprofile` on macOS (2023-01-17) - # adjust `PATH` to include `/nix/var/nix/profiles/default/bin` - if (isTRUE(is_rstudio_session()) && isFALSE(is_nix_rsession())) { - set_nix_path() - } - - has_nix_shell <- nix_shell_available() # TRUE if yes, FALSE if no - stopifnot("`nix-shell` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = - isTRUE(has_nix_shell)) - - if (isFALSE(has_nix_shell)) { - stop( - paste0("`nix-shell` is needed but is not available in your current ", - "shell environment.\n", - "* If you are in an R session of your host operating system, you - either\n1a) need to install Nix first, or if you have already done so ", - "\n", - "1b) make sure that the location of the nix store is in the `PATH` - variable of this R session (mostly necessary in RStudio).\n", - "* If you ran `with_nix()` from R launched in a `nix-shell`, you need - to make sure that `pkgs.nix` is in the `buildInput` for ", - "`pkgs.mkShell`.\nIf you used `rix::rix()` to generate your main nix - configuration of this session, just regenerate it with the additonal - argument `system_pkgs = 'nix'."), - call. = FALSE - ) - } - - program <- match.arg(program) - exec_mode <- match.arg(exec_mode) - message_type <- match.arg(message_type) - - if (program == "R") { - - # get the function arguments as a pairlist; - # save formal arguments of pairlist via `tag = value`; e.g., if we have a - # `expr = function(p = p_root) dir(path = p)`, the input object - # to be serialized will be serialized under `"p.Rds"` in a tmp dir, and - # will contain object `p_root`, which is defined in the global environment - # and bound to `"."` (project root) - args <- as.list(formals(expr)) - - cat("\n### Prepare to exchange arguments and globals for `expr`", - "between the host and Nix R sessions ###\n") - - # 1) save all function args onto a temporary folder each with - # `` and `value` as serialized objects from RAM --------------------- - temp_dir <- tempdir() - serialize_args(args, temp_dir) - - # cast list of symbols/names and calls to list of strings; this is to prepare - # deparsed version (string) of deserializing arguments from disk; - # elements of args for now should be of type "symbol" or "language" - args_vec <- vapply(args, deparse, FUN.VALUE = character(1L)) - - # todo in `rnix_deparsed`: - # => locate all global variables used by function - # https://github.com/cran/codetools/blob/master/R/codetools.R - # http://adv-r.had.co.nz/Expressions.html#ast-funs - - # code inspection: generates messages with potential problems - check_expr(expr) - - globals_expr <- recurse_find_check_globals(expr, args_vec) - - # wrapper around `serialize_lobjs()` - globals <- serialize_globals(globals_expr, temp_dir) - - # extract additional packages to export - pkgs <- serialize_pkgs(globals_expr, temp_dir) - - # 2) deserialize formal arguments of `expr` in nix session - # and necessary global objects --------------------------------------------- - # 3) serialize resulting output from evaluating function given as `expr` - - # main code to be run in nix R session - rnix_file <- file.path(temp_dir, "with_nix_r.R") - - rnix_quoted <- quote_rnix( - expr, program, message_type, args_vec, globals, pkgs, temp_dir, rnix_file - ) - rnix_deparsed <- deparse_chr1(expr = rnix_quoted, collapse = "\n") - - # 4): for 2) and 3) write script to disk, to run later via `Rscript` from - # `nix-shell` - # environment - r_version_file <- file.path(temp_dir, "nix-r-version.txt") - writeLines(text = rnix_deparsed, file(rnix_file)) - - # 3) run expression in nix session, based on temporary script - cat(paste0("==> Running deparsed expression via `nix-shell`", " in ", - exec_mode, " mode:\n\n"#, - # paste0(rnix_deparsed, collapse = " ") - )) - - # command to run deparsed R expression via nix-shell - cmd_rnix_deparsed <- c( - file.path(project_path, "default.nix"), - "--pure", # required for to have nix glibc - "--run", - sprintf( - "Rscript --vanilla '%s'", - rnix_file - ) - ) - - proc <- switch(exec_mode, - "blocking" = sys::exec_internal(cmd = "nix-shell", cmd_rnix_deparsed), - "non-blocking" = sys::exec_background( - cmd = "nix-shell", cmd_rnix_deparsed), - stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') - ) - if (exec_mode == "non-blocking") { - poll_sys_proc_nonblocking(cmd = cmd_rnix_deparsed, proc, what = "expr") - } else if (exec_mode == "blocking") { - poll_sys_proc_blocking(cmd = cmd_rnix_deparsed, proc, what = "expr") - } - } else if (program == "shell") { # end of `if (program == "R")` - shell_cmd <- c( - file.path(project_path, "default.nix"), - "--pure", - "--run", - expr - ) - proc <- switch(exec_mode, - "blocking" = sys::exec_internal(cmd = "nix-shell", shell_cmd), - "non-blocking" = sys::exec_background( - cmd = "nix-shell", shell_cmd), - stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') - ) - } - - # 5) deserialize final output of `expr` evaluated in nix-shell - # into host R session - if (program == "R") { - out <- readRDS(file = file.path(temp_dir, "_out.Rds")) - on.exit(close(file(rnix_file))) - } else if (program == "shell") { - if (exec_mode == "non-blocking") { - status <- poll_sys_proc_nonblocking( - cmd = shell_cmd, proc, what = "expr" - ) - out <- status - } else if (exec_mode == "blocking") { - poll_sys_proc_blocking(cmd = shell_cmd, proc, what = "expr") - out <- proc - out$stdout <- sys::as_text(out$stdout) - out$stderr <- sys::as_text(out$stderr) - } - } - - cat("\n### Finished code evaluation in `nix-shell` ###\n") - - # return output from evaluated function - cat("\n* Evaluating `expr` in `nix-shell` returns:\n") - if (program == "R") { - print(out) - } else if (program == "shell") { - print(out$stdout) - } - - cat("") - return(out) -} - - -#' serialize language objects -#' @noRd -serialize_lobjs <- function(lobjs, temp_dir) { - invisible({ - for (i in seq_along(lobjs)) { - if (!any(nzchar(deparse(lobjs[[i]])))) { - # for unnamed arguments like `expr = function(x) print(x)` - # x would be an empty symbol, see also ; i.e. arguments without - # default expressions; i.e. tagged arguments with no value - # https://stackoverflow.com/questions/3892580/create-missing-objects-aka-empty-symbols-empty-objects-needed-for-f - lobjs[[i]] <- as.symbol(names(lobjs)[i]) - } - saveRDS( - object = lobjs[[i]], - file = file.path(temp_dir, paste0(names(lobjs)[i], ".Rds")) - ) - } - }) -} - -serialize_args <- function(args, temp_dir) { - invisible({ - for (i in seq_along(args)) { - if (!nzchar(deparse(args[[i]]))) { - # for unnamed arguments like `expr = function(x) print(x)` - # x would be an empty symbol, see also ; i.e. arguments without - # default expressions; i.e., tagged arguments with no value - # https://stackoverflow.com/questions/3892580/create-missing-objects-aka-empty-symbols-empty-objects-needed-for-f - args[[i]] <- as.symbol(names(args)[i]) - } - args[[i]] <- get(as.character(args[[i]])) - saveRDS( - object = args[[i]], - file = file.path(temp_dir, paste0(names(args)[i], ".Rds")) - ) - } - }) -} - - -#' @noRd -check_expr <- function(expr) { - cat("* checking code in `expr` for potential problems:\n", - "`codetools::checkUsage(fun = expr)`\n") - codetools::checkUsage(fun = expr) - cat("\n") - } - - -#' @noRd -# to determine which extra packages to load in Nix R prior evaluating `expr` -get_expr_extra_pkgs <- function(globals_expr) { - envs_check <- lapply(globals_expr, where) - names_envs_check <- vapply(envs_check, environmentName, character(1L)) - - default_pkgnames <- paste0("package:", getOption("defaultPackages")) - pkgenvs_attached <- setdiff( - grep("^package:", names_envs_check, value = TRUE), - c(default_pkgnames, "base") - ) - if (!length(pkgenvs_attached) == 0L) { - pkgs_to_attach <- gsub("^package:", "", pkgenvs_attached) - return(pkgs_to_attach) - } else { - return(NULL) - } -} - - -#' @noRd -is_empty <- function(x) identical(x, emptyenv()) - - -#' @noRd -where <- function(name, env = parent.frame()) { - while(!is_empty(env)) { - if (exists(name, envir = env, inherits = FALSE)) { - return(env) - } - # inspect parent - env <- parent.env(env) - } -} - -#' Finds and checks global functions and variables recursively for closure -#' `expr` -#' @noRd -recurse_find_check_globals <- function(expr, args_vec) { - - cat("* checking code in `expr` for potential problems:\n") - codetools::checkUsage(fun = expr) - cat("\n") - - globals_expr <- codetools::findGlobals(fun = expr) - globals_lst <- classify_globals(globals_expr, args_vec) - - round_i <- 1L - - repeat { - - get_globals_exprs <- function(globals_lst) { - globals_exprs <- names(unlist(Filter(function(x) !is.null(x), - unname(globals_lst[c("globalenv_fun", "env_fun")])))) - return(globals_exprs) - } - - if (round_i == 1L) { - # first round - globals_exprs <- get_globals_exprs(globals_lst) - } else { - # successive rounds - globals_exprs <- unlist(lapply(globals_lst, get_globals_exprs)) - } - - cat("* checking code in `globals_exprs` for potential problems:\n") - lapply( - globals_exprs, - codetools::checkUsage - ) - cat("\n") - - globals_new <- lapply( - globals_exprs, - function(x) codetools::findGlobals(fun = x) - ) - - globals_lst_new <- lapply( - globals_new, - function(x) classify_globals(globals_expr = x, args_vec) - ) - - if (round_i == 1L) { - result_list <- c(list(globals_lst), globals_lst_new) - } else { - result_list <- c(result_list, globals_lst_new) - } - - # prepare current globals to find new globals one recursion level deeper - # in the call stack in the next repeat - globals_lst <- globals_lst_new - - globals_lst <- lapply(globals_lst, function(x) lapply(x, unlist)) - - # packages need to be excluded for getting more globals - globals_lst <- lapply( - globals_lst, - function(x) { - x[c("globalenv_fun", "globalenv_other", "env_other", "env_fun")] - } - ) - - globals_null <- all(is.null(unlist(globals_lst))) - # TRUE if no more candidate global values - all_non_pkgs_null <- all(globals_null) - - round_i <- round_i + 1L - - if (is.null(globals_lst) || all_non_pkgs_null) break - } - - result_list <- Filter(function(x) !is.null(x), result_list) - result_list <- lapply( - result_list, - function(x) Filter(function(x) !is.null(x), x) - ) - - pkgs <- unlist(lapply(result_list, "[", "pkgs")) - - unlist_unname <- function(x) { - unlist( - lapply(x, function(x) unlist(unname(x))) - ) - } - - globalenv_fun <- lapply(result_list, "[", "globalenv_fun") - globalenv_fun <- unlist_unname(globalenv_fun) - - globalenv_other <- lapply(result_list, "[", "globalenv_other") - globalenv_other <- unlist_unname(globalenv_other) - - env_other <- lapply(result_list, "[", "env_other") - env_other <- unlist_unname(env_other) - - env_fun = lapply(result_list, "[", "env_fun") - env_fun <- unlist_unname(env_fun) - - exports <- list( - pkgs = pkgs, - globalenv_fun = globalenv_fun, - globalenv_other = globalenv_other, - env_other = env_other, - env_fun = env_fun - ) - - return(exports) -} - -#' @noRd -classify_globals <- function(globals_expr, args_vec) { - envs_check <- lapply(globals_expr, where) - names(envs_check) <- globals_expr - - vec_envs_check <- vapply(envs_check, environmentName, character(1L)) - # directly remove formals - vec_envs_check <- vec_envs_check[!names(vec_envs_check) %in% args_vec] - if (length(vec_envs_check) == 0L) { - vec_envs_check <- NULL - } - - if (!is.null(vec_envs_check)) { - globs_pkg <- grep("^package:", vec_envs_check, value = TRUE) - if (length(globs_pkg) == 0L) { - globs_pkg <- NULL - } - # globs base can be ignored - globs_base <- grep("^base$", vec_envs_check, value = TRUE) - globs_globalenv <- grep("^R_GlobalEnv$", vec_envs_check, value = TRUE) - globs_globalenv <- Filter(nzchar, globs_globalenv) - # empty globs; can be ignored for now - globs_empty <- Filter(function(x) !nzchar(x), vec_envs_check) - if (length(globs_empty) == 0L) { - globs_empty <- NULL - } - globs_other <- vec_envs_check[!names(vec_envs_check) %in% - names(c(globs_pkg, globs_globalenv, globs_empty, globs_base))] - if (length(globs_other) == 0L) { - globs_other <- NULL - } - } - - is_globalenv_funs <- vapply( - names(globs_globalenv), function(x) is.function(get(x)), - FUN.VALUE = logical(1L) - ) - - is_otherenv_funs <- vapply( - names(globs_other), function(x) is.function(get(x)), - FUN.VALUE = logical(1L) - ) - - globs_globalenv_fun <- globs_globalenv[is_globalenv_funs] - if (length(globs_globalenv_fun) == 0L) { - globs_globalenv_fun <- NULL - } - globs_globalenv_other <- globs_globalenv[!is_globalenv_funs] - if (length(globs_globalenv_other) == 0L) { - globs_globalenv_other <- NULL - } - - globs_otherenv_fun <- globs_other[is_otherenv_funs] - if (length(globs_otherenv_fun) == 0L) { - globs_otherenv_fun <- NULL - } - globs_otherenv_other <- globs_other[!is_otherenv_funs] - if (length(globs_otherenv_other) == 0L) { - globs_otherenv_other <- NULL - } - - default_pkgnames <- paste0("package:", getOption("defaultPackages")) - pkgenvs_attached <- setdiff(globs_pkg, c(default_pkgnames, "base")) - - if (!length(pkgenvs_attached) == 0L) { - pkgs_to_attach <- gsub("^package:", "", pkgenvs_attached) - } else { - pkgs_to_attach <- NULL - } - - globs_classified <- list( - globalenv_fun = globs_globalenv_fun, - globalenv_other = globs_globalenv_other, - env_other = globs_otherenv_other, - env_fun = globs_otherenv_fun, - pkgs = pkgs_to_attach - ) - globs_null <- all(vapply(globs_classified, is.null, logical(1L))) - if (globs_null) globs_classified <- NULL - - return(globs_classified) -} - - -# wrapper to serialize expressions of all global objects found -#' @noRd -serialize_globals <- function(globals_expr, temp_dir) { - funs <- globals_expr$globalenv_fun - if (!is.null(funs)) { - cat("=> Saving global functions to disk:", paste(names(funs)), "\n") - globalenv_funs <- lapply( - names(funs), - function(x) get(x = x, envir = .GlobalEnv) - ) - names(globalenv_funs) <- names(globals_expr$globalenv_fun) - serialize_lobjs(lobjs = globalenv_funs, temp_dir) - } - others <- globals_expr$globalenv_other - if (!is.null(others)) { - cat("=> Saving non-function object(s), e.g. other environments:", - paste(names(others)), "\n" - ) - globalenv_others <- lapply( - names(others), - function(x) get(x = x, envir = .GlobalEnv) - ) - names(globalenv_others) <- names(globals_expr$globalenv_other) - serialize_lobjs(lobjs = globalenv_others, temp_dir) - } - env_funs <- globals_expr$env_fun - if (!is.null(env_funs)) { - cat("=> Serializing function(s) from other environment(s):", - paste(names(env_funs)), "\n") - env_funs <- lapply( - names(env_funs), - function(x) get(x = x) - ) - names(env_funs) <- names(globals_expr$env_fun) - serialize_lobjs(lobjs = env_funs, temp_dir) - } - env_others <- globals_expr$env_other - if (!is.null(env_others)) { - cat("=> Serializing non-function object(s) from custom environment(s)::", - paste(names(env_others)), "\n" - ) - env_others <- lapply( - names(env_others), - function(x) get(x = x) - ) - names(env_others) <- names(globals_expr$env_other) - serialize_lobjs(lobjs = env_others, temp_dir) - } - - return(c(funs, others, env_funs, env_others)) -} - - -#' @noRd -serialize_pkgs <- function(globals_expr, temp_dir) { - pkgs <- globals_expr$pkgs - if (!is.null(pkgs)) { - cat("=> Serializing package(s) required to run `expr`:\n", - paste(pkgs), "\n" - ) - } - saveRDS( - object = pkgs, - file = file.path(temp_dir, "_pkgs.Rds") - ) - return(pkgs) -} - -# build deparsed script via language objects; -# reads like R code, and avoids code injection -quote_rnix <- function(expr, - program, - message_type, - args_vec, - globals, - pkgs, - temp_dir, - rnix_file) { - expr_quoted <- bquote( { - cat("### Start evaluating `expr` in `nix-shell` ###") - cat("\n* wrote R script evaluated via `Rscript` in `nix-shell`:", - .(rnix_file)) - temp_dir <- .(temp_dir) - cat("\n", Sys.getenv("NIX_PATH")) - # fix library paths for nix R on macOS and linux; avoid permission issue - current_paths <- .libPaths() - userlib_paths <- Sys.getenv("R_LIBS_USER") - user_dir <- grep(paste(userlib_paths, collapse = "|"), current_paths) - new_paths <- current_paths[-user_dir] - .libPaths(new_paths) - r_version_num <- paste0(R.version$major, ".", R.version$minor) - cat("\n* using Nix with R version", r_version_num, "\n\n") - # assign `args_vec` as in c(...) form. - args_vec <- .(with_assign_vecnames_call(vec = args_vec)) - # deserialize arguments from disk - for (i in seq_along(args_vec)) { - nm <- args_vec[i] - obj <- args_vec[i] - assign( - x = nm, - value = readRDS(file = file.path(temp_dir, paste0(obj, ".Rds"))) - ) - cat( - paste0(" => reading file ", "'", obj, ".Rds", "'", - " for argument named `", obj, "`\n") - ) - } - - globals <- .(with_assign_vecnames_call(vec = globals)) - for (i in seq_along(globals)) { - nm <- globals[i] - obj <- globals[i] - assign( - x = nm, - value = readRDS(file = file.path(temp_dir, paste0(obj, ".Rds"))) - ) - cat( - paste0(" => reading file ", "'", obj, ".Rds", "'", - " for global object named `", obj, "`\n") - ) - } - - # for now name of character vector containing packages is hard-coded - # pkgs <- .(with_assign_vecnames_call(vec = pkgs)) - # pkgs <- .(pkgs) - pkgs <- .(with_assign_vec_call(vec = pkgs)) - lapply(pkgs, library, character.only = TRUE) - - # execute function call in `expr` with list of correct args - lst <- as.list(args_vec) - names(lst) <- args_vec - lst <- lapply(lst, as.name) - rnix_out <- do.call(.(expr), lst) - cat("\n* called `expr` with args", args_vec, ":\n") - message_type <- .(message_type) - if (message_type == "verbose") { - # cat("\n", deparse(.(expr))) # not nicely formatted, use print - # print(.(expr)) - } - cat("\n* The type of the output object returned by `expr` is", - typeof(rnix_out)) - saveRDS(object = rnix_out, file = file.path(temp_dir, "_out.Rds")) - cat("\n* Saved output to", file.path(temp_dir, "_out.Rds")) - cat("\n\n* the following objects are in the global environment:\n") - cat(ls()) - cat("\n") - cat("\n* `sessionInfo()` output:\n") - cat(capture.output(sessionInfo()), sep = "\n") - } ) # end of `bquote()` - - return(expr_quoted) -} - -# https://github.com/cran/codetools/blob/master/R/codetools.R -# finding global variables - -# reconstruct argument vector (character) in Nix R; -# build call to generate `args_vec` -#' @noRd -with_assign_vecnames_call <- function(vec) { - cl <- call("c") - for (i in seq_along(vec)) { - cl[[i + 1L]] <- names(vec[i]) - } - return(cl) -} - -#' @noRd -with_assign_vec_call <- function(vec) { - cl <- call("c") - for (i in seq_along(vec)) { - cl[[i + 1L]] <- vec[i] - } - return(cl) -} - -# this is what `deparse1()` does, however, it is only since 4.0.0 -#' @noRd -deparse_chr1 <- function(expr, width.cutoff = 500L, collapse = " ", ...) { - paste(deparse(expr, width.cutoff, ...), collapse = collapse) -} - -#' @noRd -with_expr_deparse <- function(expr) { - sprintf( - 'run_expr <- %s\n', - deparse_chr1(expr = expr, collapse = "\n") - ) -} - -#' @noRd -nix_shell_available <- function() { - which_nix_shell <- Sys.which("nix-shell") - if (nzchar(which_nix_shell)) { - return(TRUE) - } else { - return(FALSE) - } -} - -#' @noRd -create_shell_nix <- function(path = file.path("inst", "extdata", - "with_nix", "default.nix")) { - if (!dir.exists(dirname(path))) { - dir.create(dirname(path), recursive = TRUE) - } - - rix( - r_ver = "latest", - r_pkgs = NULL, - system_pkgs = NULL, - git_pkgs = NULL, - ide = "other", - project_path = dirname(path), - overwrite = TRUE, - shell_hook = NULL - ) -} -``` - -```{r, tests-with_nix} -testthat::test_that("Testing with_nix() if Nix is installed", { - - skip_if_not(nix_shell_available()) - - #skip_on_covr() - - path_subshell <- tempdir() - - rix_init( - project_path = path_subshell, - rprofile_action = "overwrite", - message_type = "simple" - ) - - rix( - r_ver = "3.5.3", - overwrite = TRUE, - project_path = path_subshell - ) - - out_subshell <- with_nix( - expr = function(){ - set.seed(1234) - a <- sample(seq(1, 10), 5) - set.seed(NULL) - return(a) - }, - program = "R", - exec_mode = "non-blocking", - project_path = path_subshell, - message_type = "simple" - ) - - # On a recent version of R, set.seed(1234);sample(seq(1,10), 5) - # returns c(10, 6, 5, 4, 1) - # but not on versions prior to 3.6 - testthat::expect_true( - all(c(2, 6, 5, 8, 9) == out_subshell) - ) - - -}) - -``` diff --git a/dev/flat_fetchers.Rmd b/dev/flat_fetchers.Rmd new file mode 100644 index 00000000..08b24c06 --- /dev/null +++ b/dev/flat_fetchers.Rmd @@ -0,0 +1,151 @@ +--- +title: "Fetchers" +output: html_document +editor_options: + chunk_output_type: console +--- + +```{r development, include=FALSE} +library(testthat) +``` + +Function `fetchgit()` takes a git repository as an input and returns a +string using the Nix `fetchgit()` function to install the package. It +automatically finds the right `sha256` as well: + +```{r function-fetchgit} +#' fetchgit Downloads and installs a package hosted of Git +#' @param git_pkg A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. +#' @return A character. The Nix definition to download and build the R package from Github. +fetchgit <- function(git_pkg){ + + package_name <- git_pkg$package_name + repo_url <- git_pkg$repo_url + branch_name <- git_pkg$branch_name + commit <- git_pkg$commit + + output <- get_sri_hash_deps(repo_url, branch_name, commit) + sri_hash <- output$sri_hash + imports <- output$deps + + sprintf('(pkgs.rPackages.buildRPackage { + name = \"%s\"; + src = pkgs.fetchgit { + url = \"%s\"; + branchName = \"%s\"; + rev = \"%s\"; + sha256 = \"%s\"; + }; + propagatedBuildInputs = builtins.attrValues { + inherit (pkgs.rPackages) %s; + }; + })', + package_name, + repo_url, + branch_name, + commit, + sri_hash, + imports +) + +} + +``` + +```{r function-fetchzip} +#' fetchzip Downloads and installs an archived CRAN package +#' @param archive_pkg A character of the form "dplyr@0.80" +#' @return A character. The Nix definition to download and build the R package from CRAN. +fetchzip <- function(archive_pkg, sri_hash = NULL){ + + pkgs <- unlist(strsplit(archive_pkg, split = "@")) + + cran_archive_link <- paste0( + "https://cran.r-project.org/src/contrib/Archive/", + pkgs[1], "/", + paste0(pkgs[1], "_", pkgs[2]), + ".tar.gz") + + package_name <- pkgs[1] + repo_url <- cran_archive_link + + if(is.null(sri_hash)){ + output <- get_sri_hash_deps(repo_url, branch_name = NULL, commit = NULL) + sri_hash <- output$sri_hash + imports <- output$deps + } else { + sri_hash <- sri_hash + imports <- NULL + } + + sprintf('(pkgs.rPackages.buildRPackage { + name = \"%s\"; + src = pkgs.fetchzip { + url = \"%s\"; + sha256 = \"%s\"; + }; + propagatedBuildInputs = builtins.attrValues { + inherit (pkgs.rPackages) %s; + }; + })', + package_name, + repo_url, + sri_hash, + imports +) +} + + +``` + +This function is a wrapper around `fetchgit()` used to handle multiple Github +packages: + +```{r function-fetchgits} +#' fetchgits Downloads and installs a packages hosted of Git. Wraps `fetchgit()` to handle multiple packages +#' @param git_pkgs A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. This argument can also be a list of lists of these four elements. +#' @return A character. The Nix definition to download and build the R package from Github. +fetchgits <- function(git_pkgs){ + + if(!all(sapply(git_pkgs, is.list))){ + fetchgit(git_pkgs) + } else if(all(sapply(git_pkgs, is.list))){ + paste(lapply(git_pkgs, fetchgit), collapse = "\n") + } else { + stop("There is something wrong with the input. Make sure it is either a list of four elements 'package_name', 'repo_url', 'branch_name' and 'commit' or a list of lists with these four elements") + } + +} +``` + +```{r function-fetchzips} +#' fetchzips Downloads and installs packages hosted in the CRAN archives. Wraps `fetchzip()` to handle multiple packages. +#' @param archive_pkgs A character, or an atomic vector of characters. +#' @return A character. The Nix definition to download and build the R package from the CRAN archives. +fetchzips <- function(archive_pkgs){ + + if(is.null(archive_pkgs)){ + "" #Empty character in case the user doesn't need any packages from the CRAN archives. + } else if(length(archive_pkgs) == 1){ + fetchzip(archive_pkgs) + } else if(length(archive_pkgs) > 1){ + paste(lapply(archive_pkgs, fetchzip), collapse = "\n") + } else { + stop("There is something wrong with the input. Make sure it is either a sinle package name, or an atomic vector of package names, for example c('dplyr@0.8.0', 'tidyr@1.0.0').") + } + +} +``` + +```{r function-fetchpkgs} +#' fetchpkgs Downloads and installs packages hosted in the CRAN archives or Github. +#' @param git_pkgs A list of four elements: "package_name", the name of the package, "repo_url", the repository's url, "branch_name", the name of the branch containing the code to download and "commit", the commit hash of interest. This argument can also be a list of lists of these four elements. +#' @param archive_pkgs A character, or an atomic vector of characters. +#' @return A character. The Nix definition to download and build the R package from the CRAN archives. +fetchpkgs <- function(git_pkgs, archive_pkgs){ + paste(fetchgits(git_pkgs), + fetchzips(archive_pkgs), + collapse = "\n") +} + +``` diff --git a/dev/flat_find_rev.Rmd b/dev/flat_find_rev.Rmd new file mode 100644 index 00000000..2f2af3fa --- /dev/null +++ b/dev/flat_find_rev.Rmd @@ -0,0 +1,72 @@ +--- +title: "Find revision" +output: html_document +editor_options: + chunk_output_type: console +--- + +```{r development, include=FALSE} +library(testthat) +``` + + + +```{r development-load} +# Load already included functions if relevant +pkgload::load_all(export_all = FALSE) +``` + +This function takes an R version as an input and returns the Nix revision that +provides it: + +```{r function-find_rev} +#' find_rev Find the right Nix revision +#' @param r_version Character. R version to look for, for example, "4.2.0". If a nixpkgs revision is provided instead, this gets returned. +#' @return A character. The Nix revision to use +#' +#' @examples +#' find_rev("4.2.0") +find_rev <- function(r_version) { + + stopifnot("r_version has to be a character." = is.character(r_version)) + + if(r_version == "latest"){ + return(get_latest()) + } else if(nchar(r_version) == 40){ + return(r_version) + } else { + + temp <- new.env(parent = emptyenv()) + + data(list = "r_nix_revs", + package = "rix", + envir = temp) + + get("r_nix_revs", envir = temp) + + output <- r_nix_revs$revision[r_nix_revs$version == r_version] + + stopifnot("Error: the provided R version is likely wrong. Please check that you provided a correct R version. You can list available versions using `available_r()`" = !identical(character(0), output)) + + output +} + +} + +``` + +```{r tests-find_rev} +testthat::test_that("find_rev returns correct nixpkgs hash", { + testthat::expect_equal( + find_rev("4.2.2"), + "8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8" + ) + + testthat::expect_equal( + find_rev("8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8"), + "8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8" + ) +}) +``` diff --git a/dev/flat_get_latest.Rmd b/dev/flat_get_latest.Rmd new file mode 100644 index 00000000..c999b095 --- /dev/null +++ b/dev/flat_get_latest.Rmd @@ -0,0 +1,37 @@ +--- +title: "Get latest R version" +output: html_document +editor_options: + chunk_output_type: console +--- + +```{r development, include=FALSE} +library(testthat) +``` + +The function below will return the very latest commit from the unstable branch +of `NixOS/nixpkgs`. This will make sure that users that want to use the most +up-to-date version of R and R packages can do so: + +```{r function-get_latest} +#' get_latest Get the latest R version and packages +#' @return A character. The commit hash of the latest nixpkgs-unstable revision +#' @importFrom httr content GET stop_for_status +#' @importFrom jsonlite fromJSON +#' +#' @examples +get_latest <- function() { + api_url <- "https://api.github.com/repos/NixOS/nixpkgs/commits?sha=nixpkgs-unstable" + + tryCatch({ + response <- httr::GET(url = api_url) + httr::stop_for_status(response) + commit_data <- jsonlite::fromJSON(httr::content(response, "text")) + latest_commit <- commit_data$sha[1] + return(latest_commit) + }, error = function(e) { + cat("Error:", e$message, "\n") + return(NULL) + }) +} +``` diff --git a/dev/flat_get_sri_hash_deps.Rmd b/dev/flat_get_sri_hash_deps.Rmd new file mode 100644 index 00000000..cc8563d4 --- /dev/null +++ b/dev/flat_get_sri_hash_deps.Rmd @@ -0,0 +1,75 @@ +--- +title: "Get SRI hash and dependencies" +output: html_document +editor_options: + chunk_output_type: console +--- + +```{r development, include=FALSE} +library(testthat) +``` + + +`get_sri_hash_deps()` returns the SRI hash of a NAR serialized path to a cloned +Github repository, or package source downloaded from the CRAN archives, alongside +that packages' build dependencies. These hashes are used by Nix for security purposes. In +order to get the hash, a GET to a service I've made gets made. This request +gets handled by a server with Nix installed, and so the SRI hash can get computed` +by `nix hash path --sri path_to_repo`. + +```{r function-get_sri_hash_deps} +#' get_sri_hash_deps Get the SRI hash of the NAR serialization of a Github repo +#' @param repo_url A character. The URL to the package's Github repository or to the `.tar.gz` package hosted on CRAN. +#' @param branch_name A character. The branch of interest, NULL for archived CRAN packages. +#' @param commit A character. The commit hash of interest, for reproducibility's sake, NULL for archived CRAN packages. +#' @importFrom httr content GET http_error +#' @return The SRI hash as a character +get_sri_hash_deps <- function(repo_url, branch_name, commit){ + result <- httr::GET(paste0("http://git2nixsha.dev:1506/hash?repo_url=", + repo_url, + "&branchName=", + branch_name, + "&commit=", + commit)) + + if(http_error(result)){ + stop(paste0("Error in pulling URL: ", repo_url, ". If it's a Github repo, check the url, branch name and commit. Are these correct? If it's an archived CRAN package, check the name of the package and the version number.")) + } + + + lapply(httr::content(result), unlist) + +} +``` + +```{r tests-get_sri_hash_deps} +testthat::test_that("get_sri_hash_deps returns correct sri hash and dependencies of R packages", { + testthat::expect_equal( + get_sri_hash_deps("https://github.com/rap4all/housing/", + "fusen", + "1c860959310b80e67c41f7bbdc3e84cef00df18e"), + list( + "sri_hash" = "sha256-s4KGtfKQ7hL0sfDhGb4BpBpspfefBN6hf+XlslqyEn4=", + "deps" = "dplyr ggplot2 janitor purrr readxl rlang rvest stringr tidyr" + ) + ) +}) + +testthat::test_that("Internet is out for fetchgit()", { + + testthat::local_mocked_bindings( + http_error = function(...) TRUE + ) + + expect_error( + get_sri_hash_deps( + "https://github.com/rap4all/housing/", + "fusen", + "1c860959310b80e67c41f7bbdc3e84cef00df18e" + ), + 'Error in pulling', + ) + +}) + +``` diff --git a/dev/flat_nix_build.Rmd b/dev/flat_nix_build.Rmd new file mode 100644 index 00000000..743fa829 --- /dev/null +++ b/dev/flat_nix_build.Rmd @@ -0,0 +1,144 @@ +--- +title: "nix_build" +output: html_document +editor_options: + chunk_output_type: console +--- + +```{r development, include=FALSE} +library(testthat) +``` + +The function below is to invoke the shell command `nix-build` from a R +session. + +```{r function-nix_build} +#' Invoke shell command `nix-build` from an R session +#' @param project_path Path to the folder where the `default.nix` file resides. +#' The default is `"."`, which is the working directory in the current R +#' session. +#' @param exec_mode Either `"blocking"` (default) or `"non-blocking`. This +#' will either block the R session while the `nix-build` shell command is +#' executed, or run `nix-build` in the background ("non-blocking"). +#' @return integer of the process ID (PID) of `nix-build` shell command +#' launched, if `nix_build()` call is assigned to an R object. Otherwise, it +#' will be returned invisibly. +#' @details The `nix-build` command line interface has more arguments. We will +#' probably not support all of them in this R wrapper, but currently we have +#' support for the following `nix-build` flags: +#' - `--max-jobs`: Maximum number of build jobs done in parallel by Nix. +#' According to the official docs of Nix, it defaults to `1`, which is one +#' core. This option can be useful for shared memory multiprocessing or +#' systems with high I/O latency. To set `--max-jobs` used, you can declare +#' with `options(rix.nix_build_max_jobs = )`. Once you call +#' `nix_build()` the flag will be propagated to the call of `nix-build`. +#' @export +#' @examples +#' \dontrun{ +#' nix_build() +#' } +nix_build <- function(project_path = ".", + exec_mode = c("blocking", "non-blocking")) { + has_nix_build <- nix_build_installed() # TRUE if yes, FALSE if no + nix_file <- file.path(project_path, "default.nix") + + stopifnot( + "`project_path` must be character of length 1." = + is.character(project_path) && length(project_path) == 1L, + "`project_path` has no `default.nix` file. Use one that contains `default.nix`" = + file.exists(nix_file), + "`nix-build` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = + isTRUE(has_nix_build) + ) + exec_mode <- match.arg(exec_mode) + + max_jobs <- getOption("rix.nix_build_max_jobs", default = 1L) + stopifnot("option `rix.nix_build_max_jobs` is not integerish" = + is_integerish(max_jobs)) + max_jobs <- as.integer(max_jobs) + + if (max_jobs == 1L) { + cmd <- c("nix-build", nix_file) + } else { + cmd <- c("nix-build", "--max-jobs", as.character(max_jobs), nix_file) + } + + cat(paste0("Launching `", paste0(cmd, collapse = " "), "`", " in ", + exec_mode, " mode\n")) + + proc <- switch(exec_mode, + "blocking" = sys::exec_internal(cmd = cmd), + "non-blocking" = sys::exec_background(cmd = cmd), + stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') + ) + + if (exec_mode == "non-blocking") { + poll_sys_proc_nonblocking(cmd, proc, what = "nix-build") + } else if (exec_mode == "blocking") { + poll_sys_proc_blocking(cmd, proc, what = "nix-build") + } + + # todo (?): clean zombies for background/non-blocking mode + + return(invisible(proc)) +} + +#' @noRd +poll_sys_proc_blocking <- function(cmd, proc, + what = c("nix-build", "expr")) { + what <- match.arg(what) + status <- proc$status + if (status == 0L) { + cat(paste0("\n==> ", sys::as_text(proc$stdout))) + cat(paste0("\n==> `", what, "` succeeded!", "\n")) + } else { + msg <- nix_build_exit_msg() + cat(paste0("`", cmd, "`", " failed with ", msg)) + } + return(invisible(status)) +} + +#' @noRd +poll_sys_proc_nonblocking <- function(cmd, proc, + what = c("nix-build", "expr")) { + what <- match.arg(what) + cat(paste0("\n==> Process ID (PID) is ", proc, ".")) + cat("\n==> Receiving stdout and stderr streams...\n") + status <- sys::exec_status(proc, wait = TRUE) + if (status == 0L) { + cat(paste0("\n==> `", what, "` succeeded!")) + } + return(invisible(status)) +} + +#' @noRd +is_integerish <- function(x, tol = .Machine$double.eps^0.5) { + return(abs(x - round(x)) < tol) +} + +#' @noRd +nix_build_installed <- function() { + exit_code <- system2("command", "-v", "nix-build") + if (exit_code == 0L) { + return(invisible(TRUE)) + } else { + return(invisible(FALSE)) + } +} + +#' @noRd +nix_build_exit_msg <- function(x) { + x_char <- as.character(x) + + err_msg <- switch( + x_char, + "100" = "generic build failure (100).", + "101" = "build timeout (101).", + "102" = "hash mismatch (102).", + "104" = "not deterministic (104).", + stop(paste0("general exit code ", x_char, ".")) + ) + + return(err_msg) +} +``` diff --git a/dev/flat_rix.Rmd b/dev/flat_rix.Rmd new file mode 100644 index 00000000..c64a521b --- /dev/null +++ b/dev/flat_rix.Rmd @@ -0,0 +1,478 @@ +--- +title: "rix" +output: html_document +editor_options: + chunk_output_type: console +--- + +```{r development, include=FALSE} +library(testthat) +``` + +This next function returns a `default.nix` file that can be used to build a +reproducible environment. This function takes an R version as an input (the +correct Nix revision is found using `find_rev()`), a list of R packages, and +whether the user wants to work with RStudio or not in that environment. If +you use another IDE, you can leave the "ide" argument blank: + +```{r function-rix} +#' rix Generates a Nix expression that builds a reproducible development environment +#' @return Nothing, this function only has the side-effect of writing a file +#' called "default.nix" in the working directory. This file contains the +#' expression to build a reproducible environment using the Nix package +#' manager. +#' @param r_ver Character, defaults to "latest". The required R version, for example "4.0.0". +#' To use the latest version of R, use "latest", if you need the latest, bleeding edge version +#' of R and packages, then use "latest". You can check which R versions are available using `available_r`. +#' For reproducibility purposes, you can also provide a nixpkgs revision. +#' @param r_pkgs Vector of characters. List the required R packages for your +#' analysis here. +#' @param system_pkgs Vector of characters. List further software you wish to install that +#' are not R packages such as command line applications for example. +#' @param git_pkgs List. A list of packages to install from Git. See details for more information. +#' @param tex_pkgs Vector of characters. A set of tex packages to install. Use this if you need to compile `.tex` documents, or build PDF documents using Quarto. If you don't know which package to add, start by adding "amsmath". See the Vignette "Authoring LaTeX documents" for more details. +#' @param ide Character, defaults to "other". If you wish to use RStudio to work +#' interactively use "rstudio" or "code" for Visual Studio Code. For other editors, +#' use "other". This has been tested with RStudio, VS Code and Emacs. If other +#' editors don't work, please open an issue. +#' @param project_path Character, defaults to the current working directory. Where to write +#' `default.nix`, for example "/home/path/to/project". +#' The file will thus be written to the file "/home/path/to/project/default.nix". +#' @param overwrite Logical, defaults to FALSE. If TRUE, overwrite the `default.nix` +#' file in the specified path. +#' @param print Logical, defaults to FALSE. If TRUE, print `default.nix` to console. +#' @param shell_hook Character, defaults to `"R --vanilla"`. Commands added to the shell_hook get +#' executed when the Nix shell starts (via `shellHook`). So by default, using `nix-shell default.nix` will +#' start R. Set to NULL if you want bash to be started instead. +#' @details This function will write a `default.nix` in the chosen path. Using +#' the Nix package manager, it is then possible to build a reproducible +#' development environment using the `nix-build` command in the path. This +#' environment will contain the chosen version of R and packages, and will not +#' interfere with any other installed version (via Nix or not) on your +#' machine. Every dependency, including both R package dependencies but also +#' system dependencies like compilers will get installed as well in that +#' environment. If you use RStudio for interactive work, then set the +#' `rstudio` parameter to `TRUE`. If you use another IDE (for example Emacs or +#' Visual Studio Code), you do not need to add it to the `default.nix` file, +#' you can simply use the version that is installed on your computer. Once you built +#' the environment using `nix-build`, you can drop into an interactive session +#' using `nix-shell`. See the "Building reproducible development environments with rix" +#' vignette for detailled instructions. +#' Packages to install from Github must be provided in a list of 4 elements: +#' "package_name", "repo_url", "branch_name" and "commit". +#' This argument can also be a list of lists of these 4 elements. It is also possible to install old versions +#' of packages by specifying a version. For example, to install the latest +#' version of `{AER}` but an old version of `{ggplot2}`, you could +#' write: `r_pkgs = c("AER", "ggplot2@2.2.1")`. Note +#' however that doing this could result in dependency hell, because an older +#' version of a package might need older versions of its dependencies, but other +#' packages might need more recent versions of the same dependencies. If instead you +#' want to use an environment as it would have looked at the time of `{ggplot2}`'s +#' version 2.2.1 release, then use the Nix revision closest to that date, by setting +#' `r_ver = "3.1.0"`, which was the version of R current at the time. This +#' ensures that Nix builds a completely coherent environment. +#' By default, the nix shell will be configured with `"en_US.UTF-8"` for the +#' relevant locale variables (`LANG`, `LC_ALL`, `LC_TIME`, `LC_MONETARY`, +#' `LC_PAPER`, `LC_MEASUREMENT`). This is done to ensure locale +#' reproducibility by default in Nix environments created with `rix()`. +#' If there are good reasons to not stick to the default, you can set your +#' preferred locale variables via +#' `options(rix.nix_locale_variables = list(LANG = "de_CH.UTF-8", <...>)` +#' and the aforementioned locale variable names. +#' @export +#' @examples +#' \dontrun{ +#' # Build an environment with the latest version of R +#' # and the dplyr and ggplot2 packages +#' rix(r_ver = "latest", +#' r_pkgs = c("dplyr", "ggplot2"), +#' system_pkgs = NULL, +#' git_pkgs = NULL, +#' ide = "code", +#' project_path = path_default_nix, +#' overwrite = TRUE, +#' print = TRUE) +#' } +rix <- function(r_ver = "latest", + r_pkgs = NULL, + system_pkgs = NULL, + git_pkgs = NULL, + tex_pkgs = NULL, + ide = "other", + project_path = ".", + overwrite = FALSE, + print = FALSE, + shell_hook = "R --vanilla"){ + + stopifnot("'ide' has to be one of 'other', 'rstudio' or 'code'" = (ide %in% c("other", "rstudio", "code"))) + + project_path <- if(project_path == "."){ + "default.nix" + } else { + paste0(project_path, "/default.nix") + } + + # Generate the correct text for the header depending on wether + # an R version or a Nix revision is supplied to `r_ver` + if(nchar(r_ver) > 20){ + r_ver_text <- paste0("as it was as of nixpkgs revision: ", r_ver) + } else { + r_ver_text <- paste0("version ", r_ver) + } + + # Find the Nix revision to use + nix_revision <- find_rev(r_ver) + + project_path <- file.path(project_path) + + rix_call <- match.call() + + generate_rix_call <- function(rix_call, nix_revision){ + + rix_call$r_ver <- nix_revision + + rix_call <- paste0("# >", deparse1(rix_call)) + + gsub(",", ",\n# >", rix_call) + } + + # Get the rix version + rix_version <- utils::packageVersion("rix") + + generate_header <- function(rix_version, + nix_revision, + r_ver_text, + rix_call){ + + if(identical(Sys.getenv("TESTTHAT"), "true")){ + sprintf(' +let + pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/%s.tar.gz") {}; +', +nix_revision) + } else { + sprintf('# This file was generated by the {rix} R package v%s on %s +# with following call: +%s +# It uses nixpkgs\' revision %s for reproducibility purposes +# which will install R %s +# Report any issues to https://github.com/b-rodrigues/rix +let + pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/%s.tar.gz") {}; +', +rix_version, +Sys.Date(), +generate_rix_call(rix_call, nix_revision), +nix_revision, +r_ver_text, +nix_revision +) + } + + } + + # Now we need to generate all the different sets of packages + # to install. Let's start by the CRAN packages, current + # and archived. The function below builds the strings. + get_rPackages <- function(r_pkgs){ + + # in case users pass something like c("dplyr", "tidyr@1.0.0") + # r_pkgs will be "dplyr" only + # and "tidyr@1.0.0" needs to be handled by fetchzips + r_and_archive_pkgs <- detect_versions(r_pkgs) + + # overwrite r_pkgs + r_pkgs <- r_and_archive_pkgs$cran_packages + + # get archive_pkgs + archive_pkgs <- r_and_archive_pkgs$archive_packages + + r_pkgs <- if(ide == "code"){ + c(r_pkgs, "languageserver") + } else { + r_pkgs + } + + rPackages <- paste(r_pkgs, collapse = ' ') + + rPackages <- gsub('\\.', '_', rPackages) + + list("rPackages" = rPackages, + "archive_pkgs" = archive_pkgs) + + } + + # Get the two lists. One list is current CRAN packages + # the other is archived CRAN packages. + cran_pkgs <- get_rPackages(r_pkgs) + + # we need to know if the user wants R packages + + flag_rpkgs <- if(is.null(cran_pkgs$rPackages) | cran_pkgs$rPackages == ""){ + "" + } else { + "rpkgs" + } + + # generate_* function generate the actual Nix code + generate_rpkgs <- function(rPackages) { + if (flag_rpkgs == ""){ + NULL + } else { + sprintf('rpkgs = builtins.attrValues { + inherit (pkgs.rPackages) %s; +}; +', +rPackages) + } + } + + # Texlive packages + generate_tex_pkgs <- function(tex_pkgs) { + if (!is.null(tex_pkgs)) { + + tex_pkgs <- paste(tex_pkgs, collapse = ' ') + + sprintf('tex = (pkgs.texlive.combine { + inherit (pkgs.texlive) scheme-small %s; +}); +', +tex_pkgs) + } + } + + flag_tex_pkgs <- if(is.null(tex_pkgs)){ + "" + } else { + "tex" + } + + # system packages + get_system_pkgs <- function(system_pkgs, r_pkgs){ + + system_pkgs <- if(any(grepl("quarto", r_pkgs))){ + unique(c(system_pkgs, "quarto")) + } else { + system_pkgs + } + + paste(system_pkgs, collapse = ' ') + } + + flag_git_archive <- if(!is.null(cran_pkgs$archive) | !is.null(git_pkgs)){ + "git_archive_pkgs" + } else { + "" + } + + generate_git_archived_packages <- function(git_pkgs, archive_pkgs){ + if(flag_git_archive == ""){ + NULL + } else { + sprintf('git_archive_pkgs = [%s];\n', + fetchpkgs(git_pkgs, archive_pkgs) + ) + } + } + + + # `R` needs to be added. If we were using the rWrapper + # this wouldn't be needed, but we're not so we need + # to add it. + generate_system_pkgs <- function(system_pkgs, r_pkgs){ + sprintf('system_packages = builtins.attrValues { + inherit (pkgs) R glibcLocales nix %s; +}; +', +get_system_pkgs(system_pkgs, r_pkgs)) + } + + generate_locale_variables <- function() { + locale_defaults <- list( + LANG = "en_US.UTF-8", + LC_ALL = "en_US.UTF-8", + LC_TIME = "en_US.UTF-8", + LC_MONETARY = "en_US.UTF-8", + LC_PAPER = "en_US.UTF-8", + LC_MEASUREMENT = "en_US.UTF-8" + ) + locale_variables <- getOption( + "rix.nix_locale_variables", + default = locale_defaults + ) + valid_vars <- all(names(locale_variables) %in% names(locale_defaults)) + if (!isTRUE(valid_vars)) { + stop("`options(rix.nix_locale_variables = list())` ", + "only allows the following element names (locale variables):\n", + paste(names(locale_defaults), collapse = "; "), + call. = FALSE) + } + locale_vars <- paste( + Map(function(x, nm) paste0(nm, ' = ', '"', x, '"'), + nm = names(locale_variables), x = locale_variables), + collapse = ";\n " + ) + paste0(locale_vars, ";\n") + } + + generate_rstudio_pkgs <- function(ide, flag_git_archive, flag_rpkgs){ + if(ide == "rstudio"){ + sprintf('rstudio_pkgs = pkgs.rstudioWrapper.override { + packages = [ %s %s ]; +}; +', +flag_git_archive, +flag_rpkgs +) + } else { + NULL + } + } + + flag_rstudio <- if (ide == "rstudio") "rstudio_pkgs" else "" + + shell_hook <- if (!is.null(shell_hook) && nzchar(shell_hook)) { + paste0('shellHook = "', shell_hook, '";') + } else {''} + + # Generate the shell + generate_shell <- function(flag_git_archive, + flag_rpkgs){ + sprintf('in + pkgs.mkShell { + %s + %s + buildInputs = [ %s %s %s system_packages %s ]; + %s + }', + generate_locale_archive(detect_os()), + generate_locale_variables(), + flag_git_archive, + flag_rpkgs, + flag_tex_pkgs, + flag_rstudio, + shell_hook + ) + + } + + # Generate default.nix file + default.nix <- paste( + generate_header(rix_version, + nix_revision, + r_ver_text, + rix_call), + generate_rpkgs(cran_pkgs$rPackages), + generate_git_archived_packages(git_pkgs, cran_pkgs$archive_pkgs), + generate_tex_pkgs(tex_pkgs), + generate_system_pkgs(system_pkgs, r_pkgs), + generate_rstudio_pkgs(ide, flag_git_archive, flag_rpkgs), + generate_shell(flag_git_archive, flag_rpkgs), + collapse = "\n" + ) + + default.nix <- readLines(textConnection(default.nix)) + + if(print){ + cat(default.nix, sep = "\n") + } + + if(!file.exists(project_path) || overwrite){ + writeLines(default.nix, project_path) + } else { + stop(paste0("File exists at ", project_path, ". Set `overwrite == TRUE` to overwrite.")) + } + + + +} +``` + +```{r, tests-rix} +testthat::test_that("Snapshot test of rix()", { + + save_default_nix_test <- function(ide) { + + path_default_nix <- tempdir() + + rix(r_ver = "4.3.1", + r_pkgs = c("dplyr", "janitor", "AER@1.2-8", "quarto"), + tex_pkgs = c("amsmath"), + git_pkgs = list( + list(package_name = "housing", + repo_url = "https://github.com/rap4all/housing/", + branch_name = "fusen", + commit = "1c860959310b80e67c41f7bbdc3e84cef00df18e"), + list(package_name = "fusen", + repo_url = "https://github.com/ThinkR-open/fusen", + branch_name = "main", + commit = "d617172447d2947efb20ad6a4463742b8a5d79dc") + ), + ide = ide, + project_path = path_default_nix, + overwrite = TRUE) + + paste0(path_default_nix, "/default.nix") + + } + + testthat::announce_snapshot_file("find_rev/rstudio_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(ide = "rstudio"), + name = "rstudio_default.nix", + ) + + testthat::announce_snapshot_file("find_rev/other_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(ide = "other"), + name = "other_default.nix" + ) + + testthat::announce_snapshot_file("find_rev/code_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(ide = "code"), + name = "code_default.nix" + ) + +}) + +``` + +```{r, tests-add_quarto_to_sys_pkgs} +testthat::test_that("Quarto gets added to sys packages", { + + save_default_nix_test <- function(pkgs) { + + path_default_nix <- tempdir() + + rix(r_ver = "4.3.1", + r_pkgs = pkgs, + ide = "other", + project_path = path_default_nix, + overwrite = TRUE) + + paste0(path_default_nix, "/default.nix") + + } + + testthat::announce_snapshot_file("find_rev/no_quarto_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(pkgs = "dplyr"), + name = "no_quarto_default.nix", + ) + + testthat::announce_snapshot_file("find_rev/yes_quarto_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(pkgs = c("dplyr", "quarto")), + name = "yes_quarto_default.nix" + ) + +}) + +``` + diff --git a/dev/flat_rix_init.Rmd b/dev/flat_rix_init.Rmd new file mode 100644 index 00000000..531517e0 --- /dev/null +++ b/dev/flat_rix_init.Rmd @@ -0,0 +1,351 @@ +--- +title: "rix_init" +output: html_document +editor_options: + chunk_output_type: console +--- + +```{r development, include=FALSE} +library(testthat) +``` + +This function bootstraps and maintains an isolated, project-specific R setup +via Nix + +```{r, function-rix_init} +#' Initiate and maintain an isolated, project-specific, and runtime-pure R +#' setup via Nix. +#' +#' Creates an isolated project folder for a Nix-R configuration. `rix::rix_init()` +#' also adds, appends, or updates with or without backup a custom `.Rprofile` +#' file with code that initializes a startup R environment without system's user +#' libraries within a Nix software environment. Instead, it restricts search +#' paths to load R packages exclusively from the Nix store. Additionally, it +#' makes Nix utilities like `nix-shell` available to run system commands from +#' the system's RStudio R session, for both Linux and macOS. +#' +#' **Enhancement of computational reproducibility for Nix-R environments:** +#' +#' The primary goal of `rix::rix_init()` is to enhance the computational +#' reproducibility of Nix-R environments during runtime. Notably, no restart is +#' required as environmental variables are set in the current session, in +#' addition to writing an `.Rprofile` file. This is particularly useful to make +#' [rix::with_nix()] evaluate custom R functions from any "Nix-to-Nix" or +#' "System-to-Nix" R setups. It introduces two side-effects that +#' take effect both in a current or later R session setup: +#' +#' 1. **Adjusting `R_LIBS_USER` path:** +#' By default, the first path of `R_LIBS_USER` points to the user library +#' outside the Nix store (see also [base::.libPaths()]). This creates +#' friction and potential impurity as R packages from the system's R user +#' library are loaded. While this feature can be useful for interactively +#' testing an R package in a Nix environment before adding it to a `.nix` +#' configuration, it can have undesired effects if not managed carefully. +#' A major drawback is that all R packages in the `R_LIBS_USER` location need +#' to be cleaned to avoid loading packages outside the Nix configuration. +#' Issues, especially on macOS, may arise due to segmentation faults or +#' incompatible linked system libraries. These problems can also occur +#' if one of the (reverse) dependencies of an R package is loaded along the +#' process. +#' +#' 2. **Make Nix commands available when running system commands from RStudio:** +#' In a host RStudio session not launched via Nix (`nix-shell`), the +#' environmental variables from `~/.zshrc` or `~/.bashrc` may not be +#' inherited. Consequently, Nix command line interfaces like `nix-shell` +#' might not be found. The `.Rprofile` code written by `rix::rix_init()` ensures +#' that Nix command line programs are accessible by adding the path of the +#' "bin" directory of the default Nix profile, +#' `"/nix/var/nix/profiles/default/bin"`, to the `PATH` variable in an +#' RStudio R session. +#' +#' These side effects are particularly recommended when working in flexible R +#' environments, especially for users who want to maintain both the system's +#' native R setup and utilize Nix expressions for reproducible development +#' environments. This init configuration is considered pivotal to enhance the +#' adoption of Nix in the R community, particularly until RStudio in Nixpkgs is +#' packaged for macOS. We recommend calling `rix::rix_init()` prior to comparing R +#' code ran between two software environments with `rix::with_nix()`. +#' +#' @param project_path Character with the folder path to the isolated nix-R project. +#' Defaults to `"."`, which is the current working directory path. If the folder +#' does not exist yet, it will be created. +#' @param rprofile_action Character. Action to take with `.Rprofile` file +#' destined for `project_path` folder. Possible values include +#' `"create_missing"`, which only writes `.Rprofile` if it +#' does not yet exist (otherwise does nothing); `"create_backup"`, which copies +#' the existing `.Rprofile` to a new backup file, generating names with +#' POSIXct-derived strings that include the time zone information. A new +#' `.Rprofile` file will be written with default code from `rix::rix_init()`; +#' `"overwrite"` overwrites the `.Rprofile` file if it does exist; `"append"` +#' appends the existing file with code that is tailored to an isolated Nix-R +#' project setup. +#' @param message_type Character. Message type, defaults to `"simple"`, which +#' gives minimal but sufficient feedback. Other values are currently +#' `"verbose"`, which provides more detailed diagnostics. +#' @export +#' @seealso [with_nix()] +#' @return Nothing, this function only has the side-effect of writing a file +#' called ".Rprofile" to the specified path. +#' @examples +#' \dontrun{ +#' # create an isolated, runtime-pure R setup via Nix +#' project_path <- "./sub_shell" +#' rix_init( +#' project_path = project_path, +#' rprofile_action = "create_missing" +#' ) +#' } +rix_init <- function(project_path = ".", + rprofile_action = c("create_missing", "create_backup", + "overwrite", "append"), + message_type = c("simple", "verbose")) { + message_type <- match.arg(message_type, choices = c("simple", "verbose")) + rprofile_action <- match.arg(rprofile_action, + choices = c("create_missing", "create_backup", "overwrite", "append")) + stopifnot( + "`project_path` needs to be character of length 1" = + is.character(project_path) && length(project_path) == 1L + ) + + cat("\n### Bootstrapping isolated, project-specific, and runtime-pure", + "R setup via Nix ###\n\n") + if (isFALSE(dir.exists(project_path))) { + dir.create(path = project_path, recursive = TRUE) + project_path <- normalizePath(path = project_path) + cat("==> Created isolated nix-R project folder:\n", project_path, "\n") + } else { + project_path <- normalizePath(path = project_path) + cat("==> Existing isolated nix-R project folder:\n", project_path, + "\n") + } + + # create project-local `.Rprofile` with pure settings + # first create the call, deparse it, and write it to .Rprofile + rprofile_quoted <- nix_rprofile() + rprofile_deparsed <- deparse_chr1(expr = rprofile_quoted, collapse = "\n") + rprofile_file <- file.path(project_path, ".Rprofile") + + rprofile_text <- get_rprofile_text(rprofile_deparsed) + write_rprofile <- function(rprofile_text, rprofile_file) { + writeLines( + text = rprofile_text, + con = file(rprofile_file) + ) + } + + is_nixr <- is_nix_rsession() + is_rstudio <- is_rstudio_session() + + rprofile_exists <- file.exists(rprofile_file) + timestamp <- format(Sys.time(), "%Y-%m-%dT%H:%M:%S%z") + rprofile_backup <- paste0(rprofile_file, "_backup_", timestamp) + + switch(rprofile_action, + create_missing = { + if (isTRUE(rprofile_exists)) { + cat( + "\n* Keep existing `.Rprofile`. in `project_path`:\n", + paste0(project_path, "/"), "\n" + ) + } else { + write_rprofile(rprofile_text, rprofile_file) + message_rprofile(action_string = "Added", project_path = project_path) + } + set_message_session_PATH(message_type = message_type) + }, + create_backup = { + if (isTRUE(rprofile_exists)) { + file.copy(from = rprofile_file, to = rprofile_backup) + cat( + "\n==> Backed up existing `.Rprofile` in file:\n", rprofile_backup, + "\n" + ) + write_rprofile(rprofile_text, rprofile_file) + message_rprofile( + action_string = "Overwrote", + project_path = project_path + ) + if (message_type == "verbose") { + cat("\n* Current lines of local `.Rprofile` are\n:") + cat(readLines(con = file(rprofile_file)), sep = "\n") + } + set_message_session_PATH(message_type = message_type) + } + }, + overwrite = { + write_rprofile(rprofile_text, rprofile_file) + if (isTRUE(rprofile_exists)) { + message_rprofile( + action_string = "Overwrote", project_path = project_path + ) + } else { + message_rprofile( + action_string = "Added", project_path = project_path + ) + } + }, + append = { + cat(paste0(rprofile_text, "\n"), file = rprofile_file, append = TRUE) + message_rprofile( + action_string = "Appended", project_path = project_path + ) + } + ) + + if (message_type == "verbose") { + cat("\n* Current lines of local `.Rprofile` are:\n\n") + cat(readLines(con = file(rprofile_file)), sep = "\n") + } + + on.exit(close(file(rprofile_file))) +} + +#' @noRd +get_rprofile_text <- function(rprofile_deparsed) { + c( +"### File generated by `rix::rix_init()` ### +# 1. Currently, system RStudio does not inherit environmental variables +# defined in `$HOME/.zshrc`, `$HOME/.bashrc` and alike. This is workaround to +# make the path of the nix store and hence basic nix commands available +# in an RStudio session +# 2. For nix-R session, remove `R_LIBS_USER`, system's R user library.`. +# This guarantees no user libraries from the system are loaded and only +# R packages in the Nix store are used. This makes Nix-R behave in pure manner +# at run-time.", + rprofile_deparsed + ) +} + +#' @noRd +message_rprofile <- function(action_string = "Added", + project_path = ".") { + msg <- paste0( + "\n==> ", action_string, + " `.Rprofile` file and code lines for new R sessions launched from:\n", + project_path, + "\n\n* Added the location of the Nix store to `PATH` ", + "environmental variable for new R sessions on host/docker RStudio:\n", + "/nix/var/nix/profiles/default/bin" + ) + cat(msg) +} + +#' @noRd +set_message_session_PATH <- function(message_type = c("simple", "verbose")) { + match.arg(message_type, choices = c("simple", "verbose")) + if (message_type == "verbose") { + cat("\n\n* Current `PATH` variable set in R session is:\n\n") + cat(Sys.getenv("PATH")) + } + cat("\n\n==> Also adjusting `PATH` via `Sys.setenv()`, so that", + "system commands can invoke key Nix commands like `nix-build` in this", + "RStudio session on the host operating system.") + PATH <- set_nix_path() + if (message_type == "verbose") { + cat("\n\n* Updated `PATH` variable is:\n\n", PATH) + } +} + +#' @noRd +is_nix_rsession <- function() { + is_nixr <- nzchar(Sys.getenv("NIX_STORE")) + if (isTRUE(is_nixr)) { + cat("==> R session running via Nix (nixpkgs)\n") + return(TRUE) + } else { + cat("\n==> R session running via host operating system or docker\n") + return(FALSE) + } +} + +#' @noRd +is_rstudio_session <- function() { + is_rstudio <- Sys.getenv("RSTUDIO") == "1" + if (isTRUE(is_rstudio)) { + cat("\n==> R session running from RStudio\n") + return(TRUE) + } else { + cat("* R session not running from RStudio") + return(FALSE) + } +} + +#' @noRd +set_nix_path <- function() { + old_path <- Sys.getenv("PATH") + nix_path <- "/nix/var/nix/profiles/default/bin" + has_nix_path <- any(grepl(nix_path, old_path)) + if (isFALSE(has_nix_path)) { + Sys.setenv( + PATH = paste(old_path, "/nix/var/nix/profiles/default/bin", sep = ":") + ) + } + invisible(Sys.getenv("PATH")) +} + +#' @noRd +nix_rprofile <- function() { + quote( { + is_rstudio <- Sys.getenv("RSTUDIO") == "1" + is_nixr <- nzchar(Sys.getenv("NIX_STORE")) + if (isFALSE(is_nixr) && isTRUE(is_rstudio)) { + # Currently, RStudio does not propagate environmental variables defined in + # `$HOME/.zshrc`, `$HOME/.bashrc` and alike. This is workaround to + # make the path of the nix store and hence basic nix commands available + # in an RStudio session + cat("{rix} detected RStudio R session") + old_path <- Sys.getenv("PATH") + nix_path <- "/nix/var/nix/profiles/default/bin" + has_nix_path <- any(grepl(nix_path, old_path)) + if (isFALSE(has_nix_path)) { + Sys.setenv( + PATH = paste( + old_path, nix_path, sep = ":" + ) + ) + } + rm(old_path, nix_path) + } + + if (isTRUE(is_nixr)) { + current_paths <- .libPaths() + userlib_paths <- Sys.getenv("R_LIBS_USER") + user_dir <- grep(paste(userlib_paths, collapse = "|"), current_paths) + new_paths <- current_paths[-user_dir] + # sets new library path without user library, making nix-R pure at + # run-time + .libPaths(new_paths) + rm(current_paths, userlib_paths, user_dir, new_paths) + } + + rm(is_rstudio, is_nixr) + } ) +} + +``` + +```{r, tests-rix_init} +testthat::test_that("Snapshot test of rix_init()", { + + save_rix_init_test <- function() { + + path_env_nix <- tempdir() + + rix_init( + project_path = path_env_nix, + rprofile_action = "overwrite", + message_type = "simple" + ) + + paste0(path_env_nix, "/.Rprofile") + + } + + testthat::announce_snapshot_file("find_rev/golden_Rprofile.txt") + + testthat::expect_snapshot_file( + path = save_rix_init_test(), + name = "golden_Rprofile.txt", + ) +}) +``` diff --git a/dev/flat_with_nix.Rmd b/dev/flat_with_nix.Rmd new file mode 100644 index 00000000..e4d02fd6 --- /dev/null +++ b/dev/flat_with_nix.Rmd @@ -0,0 +1,883 @@ +--- +title: "with_nix" +output: html_document +editor_options: + chunk_output_type: console +--- + +This function can evaluate an shell or R expression in Nix via `nix-shell` +environment. + +```{r, function-with-nix} + +#' Evaluate function in R or shell command via `nix-shell` environment +#' +#' This function needs an installation of Nix. `with_nix()` has two effects +#' to run code in isolated and reproducible environments. +#' 1. Evaluate a function in R or a shell command via the `nix-shell` +#' environment (Nix expression for custom software libraries; involving pinned +#' versions of R and R packages via Nixpkgs) +#' 2. If no error, return the result object of `expr` in `with_nix()` into the +#' current R session. +#' +#' +#' +#' `with_nix()` gives you the power of evaluating a main function `expr` +#' and its function call stack that are defined in the current R session +#' in an encapsulated nix-R session defined by Nix expression (`default.nix`), +#' which is located in at a distinct project path (`project_path`). +#' +#' `with_nix()` is very convenient because it gives direct code feedback in +#' read-eval-print-loop style, which gives a direct interface to the very +#' reproducible infrastructure-as-code approach offered by Nix and Nixpkgs. You +#' don't need extra efforts such as setting up DevOps tooling like Docker and +#' domain specific tools like {renv} to control complex software environments in +#' R and any other language. It is for example useful for the following +#' purposes. +#' +#' 1. test compatibility of custom R code and software/package dependencies in +#' development and production environments +#' 2. directly stream outputs (returned objects), messages and errors from any +#' command line tool offered in Nixpkgs into an R session. +#' 3. Test if evolving R packages change their behavior for given unchanged +#' R code, and whether they give identical results or not. +#' +#' #' `with_nix()` can evaluate both R code from a nix-R session within +#' another nix-R session, and also from a host R session (i.e., on macOS or +#' Linux) within a specific nix-R session. This feature is useful for testing +#' the reproducibility and compatibility of given code across different software +#' environments. If testing of different sets of environments is necessary, you +#' can easily do so by providing Nix expressions in custom `.nix` or +#' `default.nix` files in different subfolders of the project. +#' +#' To do its job, `with_nix()` heavily relies on patterns that manipulate +#' language expressions (aka computing on the language) offered in base R as +#' well as the {codetools} package by Luke Tierney. +#' +#' Some of the key steps that are done behind the scene: +#' 1. recursively find, classify, and export global objects (globals) in the +#' call stack of `expr` as well as propagate R package environments found. +#' 2. Serialize (save to disk) and deserialize (read from disk) dependent +#' data structures as `.Rds` with necessary function arguments provided, +#' any relevant globals in the call stack, packages, and `expr` outputs +#' returned in a temporary directory. +#' 3. Use pure `nix-shell` environments to execute a R code script +#' reconstructed catching expressions with quoting; it is launched by commands +#' like this via `{sys}` by Jeroen Ooms: +#' `nix-shell --pure --run "Rscript --vanilla"`. +#' +#' @param expr Single R function or call, or character vector of length one with +#' shell command and possibly options (flags) of the command to be invoked. +#' For `program = R`, you can both use a named or an anonymous function. +#' The function provided in `expr` should not evaluate when you pass arguments, +#' hence you need to wrap your function call like +#' `function() your_fun(arg_a = "a", arg_b = "b")`, to avoid evaluation and make +#' sure `expr` is a function (see details and examples). +#' @param program String stating where to evaluate the expression. Either `"R"`, +#' the default, or `"shell"`. `where = "R"` will evaluate the expression via +#' `RScript` and `where = "shell"` will run the system command in `nix-shell`. +#' @param exec_mode Either `"blocking"` (default) or `"non-blocking`. This +#' will either block the R session while `expr` is running in a `nix-shell` +#' environment, oor running it in the background ("non-blocking"). While +#' `program = R` will yield identical results for foreground and background +#' evaluation (R object), `program = "shell"` will return list of exit status, +#' standard output and standard error of the system command and as text in +#' blocking mode. +#' @param project_path Path to the folder where the `default.nix` file resides. +#' The default is `"."`, which is the working directory in the current R +#' session. This approach also useful when you have different subfolders +#' with separate software environments defined in different `default.nix` files. +#' If you prefer to run code in custom `.nix` files in the same directory +#' using `with_nix()`, you can use the `nix_file` argument to specify paths +#' to `.nix` files. +#' @param nix_file Path to `.nix` file that contains the expressions defining +#' the Nix software environment in which you want to run `expr`. See +#' `project_path` argument as an alternative way to specify the environment. +#' @param message_type String how detailed output is. Currently, there is +#' either `"simple"` (default) or `"verbose"`, which shows the script that runs +#' via `nix-shell`. +#' @importFrom codetools findGlobals checkUsage +#' @export +#' @return +#' - if `program = "R"`, R object returned by function given in `expr` +#' when evaluated via the R environment in `nix-shell` defined by Nix +#' expression. +#' - if `program = "shell"`, list with the following elements: +#' - `status`: exit code +#' - `stdout`: character vector with standard output +#' - `stderr`: character vector with standard error +#' of `expr` command sent to a command line interface provided by a Nix package. +#' @examples +#' \dontrun{ +#' # create an isolated, runtime-pure R setup via Nix +#' project_path <- "./sub_shell" +#' rix_init( +#' project_path = project_path, +#' rprofile_action = "create_missing" +#' ) +#' # generate nix environment in `default.nix` +#' rix( +#' r_ver = "4.2.0", +#' project_path = project_path +#' ) +#' # evaluate function in Nix-R environment via `nix-shell` and `Rscript`, +#' # stream messages, and bring output back to current R session +#' out <- with_nix( +#' expr = function(mtcars) nrow(mtcars), +#' program = "R", exec_mode = "non-blocking", project_path = project_path, +#' message_type = "simple" +#' ) +#' +#' # There no limit in the complexity of function call stacks that `with_nix()` +#' # can possibly handle; however, `expr` should not evaluate and +#' # needs to be a function for `program = "R"`. If you want to pass the +#' # a function with arguments, you can do like this +#' get_sample <- function(seed, n) { +#' set.seed(seed) +#' out <- sample(seq(1, 10), n) +#' return(out) +#' } +#' +#' out <- with_nix( +#' expr = get_sample(seed = 1234, n = 5), +#' program = "R", exec_mode = "non-blocking", +#' project_path = ".", +#' message_type = "simple" +#' ) +#' +#' #' ## You can also use packages, which will be exported to the nix-R session +#' ## running through `nix-shell` environment +#' R 4.2.2 +#' } +with_nix <- function(expr, + program = c("R", "shell"), + exec_mode = c("blocking", "non-blocking"), + project_path = ".", + nix_file = NULL, + message_type = c("simple", "verbose")) { + if (is.null(nix_file)) { + nix_file <- file.path(project_path, "default.nix") + } + stopifnot( + "`project_path` must be character of length 1." = + is.character(project_path) && length(project_path) == 1L, + "`project_path` has no `default.nix` file. Use one that contains `default.nix`" = + file.exists(nix_file), + "`message_type` must be character." = is.character(message_type), + "`expr` needs to be a call or function for `program = R`, and character of length 1 for `program = shell`" = + is.function(expr) || is.call(expr) || (is.character(expr) && length(expr) == 1L) + ) + + # ad-hoc solution for RStudio's limitation that R sessions cannot yet inherit + # proper `PATH` from custom `.Rprofile` on macOS (2023-01-17) + # adjust `PATH` to include `/nix/var/nix/profiles/default/bin` + if (isTRUE(is_rstudio_session()) && isFALSE(is_nix_rsession())) { + set_nix_path() + } + + has_nix_shell <- nix_shell_available() # TRUE if yes, FALSE if no + stopifnot("`nix-shell` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = + isTRUE(has_nix_shell)) + + if (isFALSE(has_nix_shell)) { + stop( + paste0("`nix-shell` is needed but is not available in your current ", + "shell environment.\n", + "* If you are in an R session of your host operating system, you + either\n1a) need to install Nix first, or if you have already done so ", + "\n", + "1b) make sure that the location of the nix store is in the `PATH` + variable of this R session (mostly necessary in RStudio).\n", + "* If you ran `with_nix()` from R launched in a `nix-shell`, you need + to make sure that `pkgs.nix` is in the `buildInput` for ", + "`pkgs.mkShell`.\nIf you used `rix::rix()` to generate your main nix + configuration of this session, just regenerate it with the additonal + argument `system_pkgs = 'nix'."), + call. = FALSE + ) + } + + program <- match.arg(program) + exec_mode <- match.arg(exec_mode) + message_type <- match.arg(message_type) + + if (program == "R") { + + # get the function arguments as a pairlist; + # save formal arguments of pairlist via `tag = value`; e.g., if we have a + # `expr = function(p = p_root) dir(path = p)`, the input object + # to be serialized will be serialized under `"p.Rds"` in a tmp dir, and + # will contain object `p_root`, which is defined in the global environment + # and bound to `"."` (project root) + args <- as.list(formals(expr)) + + cat("\n### Prepare to exchange arguments and globals for `expr`", + "between the host and Nix R sessions ###\n") + + # 1) save all function args onto a temporary folder each with + # `` and `value` as serialized objects from RAM --------------------- + temp_dir <- tempdir() + serialize_args(args, temp_dir) + + # cast list of symbols/names and calls to list of strings; this is to prepare + # deparsed version (string) of deserializing arguments from disk; + # elements of args for now should be of type "symbol" or "language" + args_vec <- vapply(args, deparse, FUN.VALUE = character(1L)) + + # todo in `rnix_deparsed`: + # => locate all global variables used by function + # https://github.com/cran/codetools/blob/master/R/codetools.R + # http://adv-r.had.co.nz/Expressions.html#ast-funs + + # code inspection: generates messages with potential problems + check_expr(expr) + + globals_expr <- recurse_find_check_globals(expr, args_vec) + + # wrapper around `serialize_lobjs()` + globals <- serialize_globals(globals_expr, temp_dir) + + # extract additional packages to export + pkgs <- serialize_pkgs(globals_expr, temp_dir) + + # 2) deserialize formal arguments of `expr` in nix session + # and necessary global objects --------------------------------------------- + # 3) serialize resulting output from evaluating function given as `expr` + + # main code to be run in nix R session + rnix_file <- file.path(temp_dir, "with_nix_r.R") + + rnix_quoted <- quote_rnix( + expr, program, message_type, args_vec, globals, pkgs, temp_dir, rnix_file + ) + rnix_deparsed <- deparse_chr1(expr = rnix_quoted, collapse = "\n") + + # 4): for 2) and 3) write script to disk, to run later via `Rscript` from + # `nix-shell` + # environment + r_version_file <- file.path(temp_dir, "nix-r-version.txt") + writeLines(text = rnix_deparsed, file(rnix_file)) + + # 3) run expression in nix session, based on temporary script + cat(paste0("==> Running deparsed expression via `nix-shell`", " in ", + exec_mode, " mode:\n\n"#, + # paste0(rnix_deparsed, collapse = " ") + )) + + # command to run deparsed R expression via nix-shell + cmd_rnix_deparsed <- c( + file.path(project_path, "default.nix"), + "--pure", # required for to have nix glibc + "--run", + sprintf( + "Rscript --vanilla '%s'", + rnix_file + ) + ) + + proc <- switch(exec_mode, + "blocking" = sys::exec_internal(cmd = "nix-shell", cmd_rnix_deparsed), + "non-blocking" = sys::exec_background( + cmd = "nix-shell", cmd_rnix_deparsed), + stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') + ) + if (exec_mode == "non-blocking") { + poll_sys_proc_nonblocking(cmd = cmd_rnix_deparsed, proc, what = "expr") + } else if (exec_mode == "blocking") { + poll_sys_proc_blocking(cmd = cmd_rnix_deparsed, proc, what = "expr") + } + } else if (program == "shell") { # end of `if (program == "R")` + shell_cmd <- c( + file.path(project_path, "default.nix"), + "--pure", + "--run", + expr + ) + proc <- switch(exec_mode, + "blocking" = sys::exec_internal(cmd = "nix-shell", shell_cmd), + "non-blocking" = sys::exec_background( + cmd = "nix-shell", shell_cmd), + stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') + ) + } + + # 5) deserialize final output of `expr` evaluated in nix-shell + # into host R session + if (program == "R") { + out <- readRDS(file = file.path(temp_dir, "_out.Rds")) + on.exit(close(file(rnix_file))) + } else if (program == "shell") { + if (exec_mode == "non-blocking") { + status <- poll_sys_proc_nonblocking( + cmd = shell_cmd, proc, what = "expr" + ) + out <- status + } else if (exec_mode == "blocking") { + poll_sys_proc_blocking(cmd = shell_cmd, proc, what = "expr") + out <- proc + out$stdout <- sys::as_text(out$stdout) + out$stderr <- sys::as_text(out$stderr) + } + } + + cat("\n### Finished code evaluation in `nix-shell` ###\n") + + # return output from evaluated function + cat("\n* Evaluating `expr` in `nix-shell` returns:\n") + if (program == "R") { + print(out) + } else if (program == "shell") { + print(out$stdout) + } + + cat("") + return(out) +} + + +#' serialize language objects +#' @noRd +serialize_lobjs <- function(lobjs, temp_dir) { + invisible({ + for (i in seq_along(lobjs)) { + if (!any(nzchar(deparse(lobjs[[i]])))) { + # for unnamed arguments like `expr = function(x) print(x)` + # x would be an empty symbol, see also ; i.e. arguments without + # default expressions; i.e. tagged arguments with no value + # https://stackoverflow.com/questions/3892580/create-missing-objects-aka-empty-symbols-empty-objects-needed-for-f + lobjs[[i]] <- as.symbol(names(lobjs)[i]) + } + saveRDS( + object = lobjs[[i]], + file = file.path(temp_dir, paste0(names(lobjs)[i], ".Rds")) + ) + } + }) +} + +serialize_args <- function(args, temp_dir) { + invisible({ + for (i in seq_along(args)) { + if (!nzchar(deparse(args[[i]]))) { + # for unnamed arguments like `expr = function(x) print(x)` + # x would be an empty symbol, see also ; i.e. arguments without + # default expressions; i.e., tagged arguments with no value + # https://stackoverflow.com/questions/3892580/create-missing-objects-aka-empty-symbols-empty-objects-needed-for-f + args[[i]] <- as.symbol(names(args)[i]) + } + args[[i]] <- get(as.character(args[[i]])) + saveRDS( + object = args[[i]], + file = file.path(temp_dir, paste0(names(args)[i], ".Rds")) + ) + } + }) +} + + +#' @noRd +check_expr <- function(expr) { + cat("* checking code in `expr` for potential problems:\n", + "`codetools::checkUsage(fun = expr)`\n") + codetools::checkUsage(fun = expr) + cat("\n") + } + + +#' @noRd +# to determine which extra packages to load in Nix R prior evaluating `expr` +get_expr_extra_pkgs <- function(globals_expr) { + envs_check <- lapply(globals_expr, where) + names_envs_check <- vapply(envs_check, environmentName, character(1L)) + + default_pkgnames <- paste0("package:", getOption("defaultPackages")) + pkgenvs_attached <- setdiff( + grep("^package:", names_envs_check, value = TRUE), + c(default_pkgnames, "base") + ) + if (!length(pkgenvs_attached) == 0L) { + pkgs_to_attach <- gsub("^package:", "", pkgenvs_attached) + return(pkgs_to_attach) + } else { + return(NULL) + } +} + + +#' @noRd +is_empty <- function(x) identical(x, emptyenv()) + + +#' @noRd +where <- function(name, env = parent.frame()) { + while(!is_empty(env)) { + if (exists(name, envir = env, inherits = FALSE)) { + return(env) + } + # inspect parent + env <- parent.env(env) + } +} + +#' Finds and checks global functions and variables recursively for closure +#' `expr` +#' @noRd +recurse_find_check_globals <- function(expr, args_vec) { + + cat("* checking code in `expr` for potential problems:\n") + codetools::checkUsage(fun = expr) + cat("\n") + + globals_expr <- codetools::findGlobals(fun = expr) + globals_lst <- classify_globals(globals_expr, args_vec) + + round_i <- 1L + + repeat { + + get_globals_exprs <- function(globals_lst) { + globals_exprs <- names(unlist(Filter(function(x) !is.null(x), + unname(globals_lst[c("globalenv_fun", "env_fun")])))) + return(globals_exprs) + } + + if (round_i == 1L) { + # first round + globals_exprs <- get_globals_exprs(globals_lst) + } else { + # successive rounds + globals_exprs <- unlist(lapply(globals_lst, get_globals_exprs)) + } + + cat("* checking code in `globals_exprs` for potential problems:\n") + lapply( + globals_exprs, + codetools::checkUsage + ) + cat("\n") + + globals_new <- lapply( + globals_exprs, + function(x) codetools::findGlobals(fun = x) + ) + + globals_lst_new <- lapply( + globals_new, + function(x) classify_globals(globals_expr = x, args_vec) + ) + + if (round_i == 1L) { + result_list <- c(list(globals_lst), globals_lst_new) + } else { + result_list <- c(result_list, globals_lst_new) + } + + # prepare current globals to find new globals one recursion level deeper + # in the call stack in the next repeat + globals_lst <- globals_lst_new + + globals_lst <- lapply(globals_lst, function(x) lapply(x, unlist)) + + # packages need to be excluded for getting more globals + globals_lst <- lapply( + globals_lst, + function(x) { + x[c("globalenv_fun", "globalenv_other", "env_other", "env_fun")] + } + ) + + globals_null <- all(is.null(unlist(globals_lst))) + # TRUE if no more candidate global values + all_non_pkgs_null <- all(globals_null) + + round_i <- round_i + 1L + + if (is.null(globals_lst) || all_non_pkgs_null) break + } + + result_list <- Filter(function(x) !is.null(x), result_list) + result_list <- lapply( + result_list, + function(x) Filter(function(x) !is.null(x), x) + ) + + pkgs <- unlist(lapply(result_list, "[", "pkgs")) + + unlist_unname <- function(x) { + unlist( + lapply(x, function(x) unlist(unname(x))) + ) + } + + globalenv_fun <- lapply(result_list, "[", "globalenv_fun") + globalenv_fun <- unlist_unname(globalenv_fun) + + globalenv_other <- lapply(result_list, "[", "globalenv_other") + globalenv_other <- unlist_unname(globalenv_other) + + env_other <- lapply(result_list, "[", "env_other") + env_other <- unlist_unname(env_other) + + env_fun = lapply(result_list, "[", "env_fun") + env_fun <- unlist_unname(env_fun) + + exports <- list( + pkgs = pkgs, + globalenv_fun = globalenv_fun, + globalenv_other = globalenv_other, + env_other = env_other, + env_fun = env_fun + ) + + return(exports) +} + +#' @noRd +classify_globals <- function(globals_expr, args_vec) { + envs_check <- lapply(globals_expr, where) + names(envs_check) <- globals_expr + + vec_envs_check <- vapply(envs_check, environmentName, character(1L)) + # directly remove formals + vec_envs_check <- vec_envs_check[!names(vec_envs_check) %in% args_vec] + if (length(vec_envs_check) == 0L) { + vec_envs_check <- NULL + } + + if (!is.null(vec_envs_check)) { + globs_pkg <- grep("^package:", vec_envs_check, value = TRUE) + if (length(globs_pkg) == 0L) { + globs_pkg <- NULL + } + # globs base can be ignored + globs_base <- grep("^base$", vec_envs_check, value = TRUE) + globs_globalenv <- grep("^R_GlobalEnv$", vec_envs_check, value = TRUE) + globs_globalenv <- Filter(nzchar, globs_globalenv) + # empty globs; can be ignored for now + globs_empty <- Filter(function(x) !nzchar(x), vec_envs_check) + if (length(globs_empty) == 0L) { + globs_empty <- NULL + } + globs_other <- vec_envs_check[!names(vec_envs_check) %in% + names(c(globs_pkg, globs_globalenv, globs_empty, globs_base))] + if (length(globs_other) == 0L) { + globs_other <- NULL + } + } + + is_globalenv_funs <- vapply( + names(globs_globalenv), function(x) is.function(get(x)), + FUN.VALUE = logical(1L) + ) + + is_otherenv_funs <- vapply( + names(globs_other), function(x) is.function(get(x)), + FUN.VALUE = logical(1L) + ) + + globs_globalenv_fun <- globs_globalenv[is_globalenv_funs] + if (length(globs_globalenv_fun) == 0L) { + globs_globalenv_fun <- NULL + } + globs_globalenv_other <- globs_globalenv[!is_globalenv_funs] + if (length(globs_globalenv_other) == 0L) { + globs_globalenv_other <- NULL + } + + globs_otherenv_fun <- globs_other[is_otherenv_funs] + if (length(globs_otherenv_fun) == 0L) { + globs_otherenv_fun <- NULL + } + globs_otherenv_other <- globs_other[!is_otherenv_funs] + if (length(globs_otherenv_other) == 0L) { + globs_otherenv_other <- NULL + } + + default_pkgnames <- paste0("package:", getOption("defaultPackages")) + pkgenvs_attached <- setdiff(globs_pkg, c(default_pkgnames, "base")) + + if (!length(pkgenvs_attached) == 0L) { + pkgs_to_attach <- gsub("^package:", "", pkgenvs_attached) + } else { + pkgs_to_attach <- NULL + } + + globs_classified <- list( + globalenv_fun = globs_globalenv_fun, + globalenv_other = globs_globalenv_other, + env_other = globs_otherenv_other, + env_fun = globs_otherenv_fun, + pkgs = pkgs_to_attach + ) + globs_null <- all(vapply(globs_classified, is.null, logical(1L))) + if (globs_null) globs_classified <- NULL + + return(globs_classified) +} + + +# wrapper to serialize expressions of all global objects found +#' @noRd +serialize_globals <- function(globals_expr, temp_dir) { + funs <- globals_expr$globalenv_fun + if (!is.null(funs)) { + cat("=> Saving global functions to disk:", paste(names(funs)), "\n") + globalenv_funs <- lapply( + names(funs), + function(x) get(x = x, envir = .GlobalEnv) + ) + names(globalenv_funs) <- names(globals_expr$globalenv_fun) + serialize_lobjs(lobjs = globalenv_funs, temp_dir) + } + others <- globals_expr$globalenv_other + if (!is.null(others)) { + cat("=> Saving non-function object(s), e.g. other environments:", + paste(names(others)), "\n" + ) + globalenv_others <- lapply( + names(others), + function(x) get(x = x, envir = .GlobalEnv) + ) + names(globalenv_others) <- names(globals_expr$globalenv_other) + serialize_lobjs(lobjs = globalenv_others, temp_dir) + } + env_funs <- globals_expr$env_fun + if (!is.null(env_funs)) { + cat("=> Serializing function(s) from other environment(s):", + paste(names(env_funs)), "\n") + env_funs <- lapply( + names(env_funs), + function(x) get(x = x) + ) + names(env_funs) <- names(globals_expr$env_fun) + serialize_lobjs(lobjs = env_funs, temp_dir) + } + env_others <- globals_expr$env_other + if (!is.null(env_others)) { + cat("=> Serializing non-function object(s) from custom environment(s)::", + paste(names(env_others)), "\n" + ) + env_others <- lapply( + names(env_others), + function(x) get(x = x) + ) + names(env_others) <- names(globals_expr$env_other) + serialize_lobjs(lobjs = env_others, temp_dir) + } + + return(c(funs, others, env_funs, env_others)) +} + + +#' @noRd +serialize_pkgs <- function(globals_expr, temp_dir) { + pkgs <- globals_expr$pkgs + if (!is.null(pkgs)) { + cat("=> Serializing package(s) required to run `expr`:\n", + paste(pkgs), "\n" + ) + } + saveRDS( + object = pkgs, + file = file.path(temp_dir, "_pkgs.Rds") + ) + return(pkgs) +} + +# build deparsed script via language objects; +# reads like R code, and avoids code injection +quote_rnix <- function(expr, + program, + message_type, + args_vec, + globals, + pkgs, + temp_dir, + rnix_file) { + expr_quoted <- bquote( { + cat("### Start evaluating `expr` in `nix-shell` ###") + cat("\n* wrote R script evaluated via `Rscript` in `nix-shell`:", + .(rnix_file)) + temp_dir <- .(temp_dir) + cat("\n", Sys.getenv("NIX_PATH")) + # fix library paths for nix R on macOS and linux; avoid permission issue + current_paths <- .libPaths() + userlib_paths <- Sys.getenv("R_LIBS_USER") + user_dir <- grep(paste(userlib_paths, collapse = "|"), current_paths) + new_paths <- current_paths[-user_dir] + .libPaths(new_paths) + r_version_num <- paste0(R.version$major, ".", R.version$minor) + cat("\n* using Nix with R version", r_version_num, "\n\n") + # assign `args_vec` as in c(...) form. + args_vec <- .(with_assign_vecnames_call(vec = args_vec)) + # deserialize arguments from disk + for (i in seq_along(args_vec)) { + nm <- args_vec[i] + obj <- args_vec[i] + assign( + x = nm, + value = readRDS(file = file.path(temp_dir, paste0(obj, ".Rds"))) + ) + cat( + paste0(" => reading file ", "'", obj, ".Rds", "'", + " for argument named `", obj, "`\n") + ) + } + + globals <- .(with_assign_vecnames_call(vec = globals)) + for (i in seq_along(globals)) { + nm <- globals[i] + obj <- globals[i] + assign( + x = nm, + value = readRDS(file = file.path(temp_dir, paste0(obj, ".Rds"))) + ) + cat( + paste0(" => reading file ", "'", obj, ".Rds", "'", + " for global object named `", obj, "`\n") + ) + } + + # for now name of character vector containing packages is hard-coded + # pkgs <- .(with_assign_vecnames_call(vec = pkgs)) + # pkgs <- .(pkgs) + pkgs <- .(with_assign_vec_call(vec = pkgs)) + lapply(pkgs, library, character.only = TRUE) + + # execute function call in `expr` with list of correct args + lst <- as.list(args_vec) + names(lst) <- args_vec + lst <- lapply(lst, as.name) + rnix_out <- do.call(.(expr), lst) + cat("\n* called `expr` with args", args_vec, ":\n") + message_type <- .(message_type) + if (message_type == "verbose") { + # cat("\n", deparse(.(expr))) # not nicely formatted, use print + # print(.(expr)) + } + cat("\n* The type of the output object returned by `expr` is", + typeof(rnix_out)) + saveRDS(object = rnix_out, file = file.path(temp_dir, "_out.Rds")) + cat("\n* Saved output to", file.path(temp_dir, "_out.Rds")) + cat("\n\n* the following objects are in the global environment:\n") + cat(ls()) + cat("\n") + cat("\n* `sessionInfo()` output:\n") + cat(capture.output(sessionInfo()), sep = "\n") + } ) # end of `bquote()` + + return(expr_quoted) +} + +# https://github.com/cran/codetools/blob/master/R/codetools.R +# finding global variables + +# reconstruct argument vector (character) in Nix R; +# build call to generate `args_vec` +#' @noRd +with_assign_vecnames_call <- function(vec) { + cl <- call("c") + for (i in seq_along(vec)) { + cl[[i + 1L]] <- names(vec[i]) + } + return(cl) +} + +#' @noRd +with_assign_vec_call <- function(vec) { + cl <- call("c") + for (i in seq_along(vec)) { + cl[[i + 1L]] <- vec[i] + } + return(cl) +} + +# this is what `deparse1()` does, however, it is only since 4.0.0 +#' @noRd +deparse_chr1 <- function(expr, width.cutoff = 500L, collapse = " ", ...) { + paste(deparse(expr, width.cutoff, ...), collapse = collapse) +} + +#' @noRd +with_expr_deparse <- function(expr) { + sprintf( + 'run_expr <- %s\n', + deparse_chr1(expr = expr, collapse = "\n") + ) +} + +#' @noRd +nix_shell_available <- function() { + which_nix_shell <- Sys.which("nix-shell") + if (nzchar(which_nix_shell)) { + return(TRUE) + } else { + return(FALSE) + } +} + +#' @noRd +create_shell_nix <- function(path = file.path("inst", "extdata", + "with_nix", "default.nix")) { + if (!dir.exists(dirname(path))) { + dir.create(dirname(path), recursive = TRUE) + } + + rix( + r_ver = "latest", + r_pkgs = NULL, + system_pkgs = NULL, + git_pkgs = NULL, + ide = "other", + project_path = dirname(path), + overwrite = TRUE, + shell_hook = NULL + ) +} +``` + +```{r, tests-with_nix} +testthat::test_that("Testing with_nix() if Nix is installed", { + + skip_if_not(nix_shell_available()) + + skip_on_covr() + + path_subshell <- tempdir() + + rix_init( + project_path = path_subshell, + rprofile_action = "overwrite", + message_type = "simple" + ) + + rix( + r_ver = "3.5.3", + overwrite = TRUE, + project_path = path_subshell + ) + + out_subshell <- with_nix( + expr = function(){ + set.seed(1234) + a <- sample(seq(1, 10), 5) + set.seed(NULL) + return(a) + }, + program = "R", + exec_mode = "non-blocking", + project_path = path_subshell, + message_type = "simple" + ) + + # On a recent version of R, set.seed(1234);sample(seq(1,10), 5) + # returns c(10, 6, 5, 4, 1) + # but not on versions prior to 3.6 + testthat::expect_true( + all(c(2, 6, 5, 8, 9) == out_subshell) + ) + + +}) + +``` diff --git a/dev/literate_programming.Rmd b/dev/z-literate_programming.Rmd similarity index 100% rename from dev/literate_programming.Rmd rename to dev/z-literate_programming.Rmd diff --git a/dev/pkgs_with_remotes.Rmd b/dev/z-pkgs_with_remotes.Rmd similarity index 100% rename from dev/pkgs_with_remotes.Rmd rename to dev/z-pkgs_with_remotes.Rmd diff --git a/dev/raps_with_nix.Rmd b/dev/z-raps_with_nix.Rmd similarity index 100% rename from dev/raps_with_nix.Rmd rename to dev/z-raps_with_nix.Rmd diff --git a/dev/subshells.Rmd b/dev/z-subshells.Rmd similarity index 100% rename from dev/subshells.Rmd rename to dev/z-subshells.Rmd diff --git a/inst/extdata/default.nix b/inst/extdata/default.nix index 386786bf..ed6b84ac 100644 --- a/inst/extdata/default.nix +++ b/inst/extdata/default.nix @@ -1,35 +1,34 @@ -# This file was generated by the {rix} R package v0.5.0 on 2024-01-16 +# This file was generated by the {rix} R package v0.5.1.9000 on 2024-01-29 # with following call: -# >rix(r_ver = "e0629618b4b419a47e2c8a3cab223e2a7f3a8f97", +# >rix(r_ver = "902d74314fae5eb824bc7b597bd4d39640345557", # > r_pkgs = NULL, # > system_pkgs = NULL, -# > git_pkgs = list(list(package_name = "rix", -# > repo_url = "https://github.com/b-rodrigues/rix", +# > git_pkgs = list(package_name = "rix", +# > repo_url = "https://github.com/b-rodrigues/rix/", # > branch_name = "master", -# > commit = "22711ee98c0092e56c620122800ca8f30b773a65")), +# > commit = "11f6898fde38a4793a7f53900c88d2fd930e882f"), # > ide = "other", -# > project_path = dirname(path), -# > overwrite = TRUE, -# > shell_hook = "R --vanilla") -# It uses nixpkgs' revision e0629618b4b419a47e2c8a3cab223e2a7f3a8f97 for reproducibility purposes +# > project_path = "inst/extdata", +# > overwrite = TRUE) +# It uses nixpkgs' revision 902d74314fae5eb824bc7b597bd4d39640345557 for reproducibility purposes # which will install R version latest # Report any issues to https://github.com/b-rodrigues/rix let - pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/e0629618b4b419a47e2c8a3cab223e2a7f3a8f97.tar.gz") {}; + pkgs = import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/902d74314fae5eb824bc7b597bd4d39640345557.tar.gz") {}; git_archive_pkgs = [(pkgs.rPackages.buildRPackage { name = "rix"; src = pkgs.fetchgit { - url = "https://github.com/b-rodrigues/rix"; + url = "https://github.com/b-rodrigues/rix/"; branchName = "master"; - rev = "22711ee98c0092e56c620122800ca8f30b773a65"; - sha256 = "sha256-uCyvALQ7F6qHh8/c0VS7T+/svtGEMEg2f7/+sUinbMo="; + rev = "11f6898fde38a4793a7f53900c88d2fd930e882f"; + sha256 = "sha256-8Svbdu2qySZRfcXmf6PcqdtSJoHI2t9nC2XUUS9KuRo="; }; propagatedBuildInputs = builtins.attrValues { inherit (pkgs.rPackages) codetools httr jsonlite sys; }; }) ]; system_packages = builtins.attrValues { - inherit (pkgs) R glibcLocales ; + inherit (pkgs) R glibcLocales nix ; }; in pkgs.mkShell { diff --git a/man/available_r.Rd b/man/available_r.Rd index ebdb802f..73aa7476 100644 --- a/man/available_r.Rd +++ b/man/available_r.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/find_rev.R +% Please edit documentation in R/available_r.R \name{available_r} \alias{available_r} \title{List available R versions from Nixpkgs} diff --git a/man/nix_build.Rd b/man/nix_build.Rd index 5b56d52e..b1540a18 100644 --- a/man/nix_build.Rd +++ b/man/nix_build.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/find_rev.R +% Please edit documentation in R/nix_build.R \name{nix_build} \alias{nix_build} \title{Invoke shell command \code{nix-build} from an R session} diff --git a/man/rix.Rd b/man/rix.Rd index d589ac32..99be74f5 100644 --- a/man/rix.Rd +++ b/man/rix.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/find_rev.R +% Please edit documentation in R/rix.R \name{rix} \alias{rix} \title{rix Generates a Nix expression that builds a reproducible development environment} diff --git a/man/rix_init.Rd b/man/rix_init.Rd index b4551ee0..f4ac0c8a 100644 --- a/man/rix_init.Rd +++ b/man/rix_init.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/find_rev.R +% Please edit documentation in R/rix_init.R \name{rix_init} \alias{rix_init} \title{Initiate and maintain an isolated, project-specific, and runtime-pure R diff --git a/man/with_nix.Rd b/man/with_nix.Rd index d6e5319a..74710b8d 100644 --- a/man/with_nix.Rd +++ b/man/with_nix.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/find_rev.R +% Please edit documentation in R/with_nix.R \name{with_nix} \alias{with_nix} \title{Evaluate function in R or shell command via \code{nix-shell} environment} diff --git a/tests/testthat/test-available_r.R b/tests/testthat/test-available_r.R new file mode 100644 index 00000000..e74446fb --- /dev/null +++ b/tests/testthat/test-available_r.R @@ -0,0 +1,15 @@ +# WARNING - Generated by {fusen} from dev/flat_available_R.Rmd: do not edit by hand + +testthat::test_that("available_r lists all available r versions", { + testthat::expect_equal( + available_r(), + c("latest", "3.0.2", "3.0.3", "3.1.0", "3.1.2", "3.1.3", "3.2.0", "3.2.1", + "3.2.2", "3.2.3", "3.2.4", "3.3.3", "3.4.0", "3.4.1", "3.4.2", "3.4.3", + "3.4.4", "3.5.0", "3.5.1", "3.5.2", "3.5.3", "3.6.0", "3.6.1", "3.6.2", + "3.6.3", "4.0.0", "4.0.2", "4.0.3", "4.0.4", "4.1.1", "4.1.2", "4.1.3", + "4.2.0", "4.2.1", "4.2.2", "4.2.3", "4.3.1" + ) + ) +}) + + diff --git a/tests/testthat/test-find_rev.R b/tests/testthat/test-find_rev.R index 7e7207b7..e275eca3 100644 --- a/tests/testthat/test-find_rev.R +++ b/tests/testthat/test-find_rev.R @@ -1,4 +1,4 @@ -# WARNING - Generated by {fusen} from dev/flat_build_envs.Rmd: do not edit by hand +# WARNING - Generated by {fusen} from dev/flat_find_rev.Rmd: do not edit by hand testthat::test_that("find_rev returns correct nixpkgs hash", { testthat::expect_equal( @@ -11,201 +11,3 @@ testthat::test_that("find_rev returns correct nixpkgs hash", { "8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8" ) }) - -testthat::test_that("available_r lists all available r versions", { - testthat::expect_equal( - available_r(), - c("latest", "3.0.2", "3.0.3", "3.1.0", "3.1.2", "3.1.3", "3.2.0", "3.2.1", - "3.2.2", "3.2.3", "3.2.4", "3.3.3", "3.4.0", "3.4.1", "3.4.2", "3.4.3", - "3.4.4", "3.5.0", "3.5.1", "3.5.2", "3.5.3", "3.6.0", "3.6.1", "3.6.2", - "3.6.3", "4.0.0", "4.0.2", "4.0.3", "4.0.4", "4.1.1", "4.1.2", "4.1.3", - "4.2.0", "4.2.1", "4.2.2", "4.2.3", "4.3.1" - ) - ) -}) - - - -testthat::test_that("get_sri_hash_deps returns correct sri hash and dependencies of R packages", { - testthat::expect_equal( - get_sri_hash_deps("https://github.com/rap4all/housing/", - "fusen", - "1c860959310b80e67c41f7bbdc3e84cef00df18e"), - list( - "sri_hash" = "sha256-s4KGtfKQ7hL0sfDhGb4BpBpspfefBN6hf+XlslqyEn4=", - "deps" = "dplyr ggplot2 janitor purrr readxl rlang rvest stringr tidyr" - ) - ) -}) - -testthat::test_that("Internet is out for fetchgit()", { - - testthat::local_mocked_bindings( - http_error = function(...) TRUE - ) - - expect_error( - get_sri_hash_deps( - "https://github.com/rap4all/housing/", - "fusen", - "1c860959310b80e67c41f7bbdc3e84cef00df18e" - ), - 'Error in pulling', - ) - -}) - - -testthat::test_that("Snapshot test of rix()", { - - save_default_nix_test <- function(ide) { - - path_default_nix <- tempdir() - - rix(r_ver = "4.3.1", - r_pkgs = c("dplyr", "janitor", "AER@1.2-8", "quarto"), - tex_pkgs = c("amsmath"), - git_pkgs = list( - list(package_name = "housing", - repo_url = "https://github.com/rap4all/housing/", - branch_name = "fusen", - commit = "1c860959310b80e67c41f7bbdc3e84cef00df18e"), - list(package_name = "fusen", - repo_url = "https://github.com/ThinkR-open/fusen", - branch_name = "main", - commit = "d617172447d2947efb20ad6a4463742b8a5d79dc") - ), - ide = ide, - project_path = path_default_nix, - overwrite = TRUE) - - paste0(path_default_nix, "/default.nix") - - } - - testthat::announce_snapshot_file("find_rev/rstudio_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(ide = "rstudio"), - name = "rstudio_default.nix", - ) - - testthat::announce_snapshot_file("find_rev/other_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(ide = "other"), - name = "other_default.nix" - ) - - testthat::announce_snapshot_file("find_rev/code_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(ide = "code"), - name = "code_default.nix" - ) - -}) - - -testthat::test_that("Quarto gets added to sys packages", { - - save_default_nix_test <- function(pkgs) { - - path_default_nix <- tempdir() - - rix(r_ver = "4.3.1", - r_pkgs = pkgs, - ide = "other", - project_path = path_default_nix, - overwrite = TRUE) - - paste0(path_default_nix, "/default.nix") - - } - - testthat::announce_snapshot_file("find_rev/no_quarto_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(pkgs = "dplyr"), - name = "no_quarto_default.nix", - ) - - testthat::announce_snapshot_file("find_rev/yes_quarto_default.nix") - - testthat::expect_snapshot_file( - path = save_default_nix_test(pkgs = c("dplyr", "quarto")), - name = "yes_quarto_default.nix" - ) - -}) - - -testthat::test_that("Snapshot test of rix_init()", { - - #skip_on_covr() - - save_rix_init_test <- function() { - - path_env_nix <- tempdir() - - rix_init( - project_path = path_env_nix, - rprofile_action = "overwrite", - message_type = "simple" - ) - - paste0(path_env_nix, "/.Rprofile") - - } - - testthat::announce_snapshot_file("find_rev/golden_Rprofile.txt") - - testthat::expect_snapshot_file( - path = save_rix_init_test(), - name = "golden_Rprofile.txt", - ) -}) - -testthat::test_that("Testing with_nix() if Nix is installed", { - - skip_if_not(nix_shell_available()) - - #skip_on_covr() - - path_subshell <- tempdir() - - rix_init( - project_path = path_subshell, - rprofile_action = "overwrite", - message_type = "simple" - ) - - rix( - r_ver = "3.5.3", - overwrite = TRUE, - project_path = path_subshell - ) - - out_subshell <- with_nix( - expr = function(){ - set.seed(1234) - a <- sample(seq(1, 10), 5) - set.seed(NULL) - return(a) - }, - program = "R", - exec_mode = "non-blocking", - project_path = path_subshell, - message_type = "simple" - ) - - # On a recent version of R, set.seed(1234);sample(seq(1,10), 5) - # returns c(10, 6, 5, 4, 1) - # but not on versions prior to 3.6 - testthat::expect_true( - all(c(2, 6, 5, 8, 9) == out_subshell) - ) - - -}) - diff --git a/tests/testthat/test-get_sri_hash_deps.R b/tests/testthat/test-get_sri_hash_deps.R new file mode 100644 index 00000000..9b059c5d --- /dev/null +++ b/tests/testthat/test-get_sri_hash_deps.R @@ -0,0 +1,31 @@ +# WARNING - Generated by {fusen} from dev/flat_get_sri_hash_deps.Rmd: do not edit by hand + +testthat::test_that("get_sri_hash_deps returns correct sri hash and dependencies of R packages", { + testthat::expect_equal( + get_sri_hash_deps("https://github.com/rap4all/housing/", + "fusen", + "1c860959310b80e67c41f7bbdc3e84cef00df18e"), + list( + "sri_hash" = "sha256-s4KGtfKQ7hL0sfDhGb4BpBpspfefBN6hf+XlslqyEn4=", + "deps" = "dplyr ggplot2 janitor purrr readxl rlang rvest stringr tidyr" + ) + ) +}) + +testthat::test_that("Internet is out for fetchgit()", { + + testthat::local_mocked_bindings( + http_error = function(...) TRUE + ) + + expect_error( + get_sri_hash_deps( + "https://github.com/rap4all/housing/", + "fusen", + "1c860959310b80e67c41f7bbdc3e84cef00df18e" + ), + 'Error in pulling', + ) + +}) + diff --git a/tests/testthat/test-rix.R b/tests/testthat/test-rix.R new file mode 100644 index 00000000..298079e5 --- /dev/null +++ b/tests/testthat/test-rix.R @@ -0,0 +1,85 @@ +# WARNING - Generated by {fusen} from dev/flat_rix.Rmd: do not edit by hand + +testthat::test_that("Snapshot test of rix()", { + + save_default_nix_test <- function(ide) { + + path_default_nix <- tempdir() + + rix(r_ver = "4.3.1", + r_pkgs = c("dplyr", "janitor", "AER@1.2-8", "quarto"), + tex_pkgs = c("amsmath"), + git_pkgs = list( + list(package_name = "housing", + repo_url = "https://github.com/rap4all/housing/", + branch_name = "fusen", + commit = "1c860959310b80e67c41f7bbdc3e84cef00df18e"), + list(package_name = "fusen", + repo_url = "https://github.com/ThinkR-open/fusen", + branch_name = "main", + commit = "d617172447d2947efb20ad6a4463742b8a5d79dc") + ), + ide = ide, + project_path = path_default_nix, + overwrite = TRUE) + + paste0(path_default_nix, "/default.nix") + + } + + testthat::announce_snapshot_file("find_rev/rstudio_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(ide = "rstudio"), + name = "rstudio_default.nix", + ) + + testthat::announce_snapshot_file("find_rev/other_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(ide = "other"), + name = "other_default.nix" + ) + + testthat::announce_snapshot_file("find_rev/code_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(ide = "code"), + name = "code_default.nix" + ) + +}) + + +testthat::test_that("Quarto gets added to sys packages", { + + save_default_nix_test <- function(pkgs) { + + path_default_nix <- tempdir() + + rix(r_ver = "4.3.1", + r_pkgs = pkgs, + ide = "other", + project_path = path_default_nix, + overwrite = TRUE) + + paste0(path_default_nix, "/default.nix") + + } + + testthat::announce_snapshot_file("find_rev/no_quarto_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(pkgs = "dplyr"), + name = "no_quarto_default.nix", + ) + + testthat::announce_snapshot_file("find_rev/yes_quarto_default.nix") + + testthat::expect_snapshot_file( + path = save_default_nix_test(pkgs = c("dplyr", "quarto")), + name = "yes_quarto_default.nix" + ) + +}) + diff --git a/tests/testthat/test-rix_init.R b/tests/testthat/test-rix_init.R new file mode 100644 index 00000000..645a738d --- /dev/null +++ b/tests/testthat/test-rix_init.R @@ -0,0 +1,25 @@ +# WARNING - Generated by {fusen} from dev/flat_rix_init.Rmd: do not edit by hand + +testthat::test_that("Snapshot test of rix_init()", { + + save_rix_init_test <- function() { + + path_env_nix <- tempdir() + + rix_init( + project_path = path_env_nix, + rprofile_action = "overwrite", + message_type = "simple" + ) + + paste0(path_env_nix, "/.Rprofile") + + } + + testthat::announce_snapshot_file("find_rev/golden_Rprofile.txt") + + testthat::expect_snapshot_file( + path = save_rix_init_test(), + name = "golden_Rprofile.txt", + ) +}) diff --git a/tests/testthat/test-with_nix.R b/tests/testthat/test-with_nix.R new file mode 100644 index 00000000..ba20c7fd --- /dev/null +++ b/tests/testthat/test-with_nix.R @@ -0,0 +1,45 @@ +# WARNING - Generated by {fusen} from dev/flat_with_nix.Rmd: do not edit by hand + +testthat::test_that("Testing with_nix() if Nix is installed", { + + skip_if_not(nix_shell_available()) + + skip_on_covr() + + path_subshell <- tempdir() + + rix_init( + project_path = path_subshell, + rprofile_action = "overwrite", + message_type = "simple" + ) + + rix( + r_ver = "3.5.3", + overwrite = TRUE, + project_path = path_subshell + ) + + out_subshell <- with_nix( + expr = function(){ + set.seed(1234) + a <- sample(seq(1, 10), 5) + set.seed(NULL) + return(a) + }, + program = "R", + exec_mode = "non-blocking", + project_path = path_subshell, + message_type = "simple" + ) + + # On a recent version of R, set.seed(1234);sample(seq(1,10), 5) + # returns c(10, 6, 5, 4, 1) + # but not on versions prior to 3.6 + testthat::expect_true( + all(c(2, 6, 5, 8, 9) == out_subshell) + ) + + +}) + diff --git a/vignettes/a-getting-started.Rmd b/vignettes/a-getting-started.Rmd index 6a0c571d..32940486 100644 --- a/vignettes/a-getting-started.Rmd +++ b/vignettes/a-getting-started.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + ## The Nix package manager diff --git a/vignettes/b1-setting-up-and-using-rix-on-linux-and-windows.Rmd b/vignettes/b1-setting-up-and-using-rix-on-linux-and-windows.Rmd index e1a89fa4..06dbb2e7 100644 --- a/vignettes/b1-setting-up-and-using-rix-on-linux-and-windows.Rmd +++ b/vignettes/b1-setting-up-and-using-rix-on-linux-and-windows.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + *This vignette will discuss Linux and Windows-specific topics. If you're not using either of these systems, you can ignore this vignette, and read the diff --git a/vignettes/b2-setting-up-and-using-rix-on-macos.Rmd b/vignettes/b2-setting-up-and-using-rix-on-macos.Rmd index 59436a94..f1fc1340 100644 --- a/vignettes/b2-setting-up-and-using-rix-on-macos.Rmd +++ b/vignettes/b2-setting-up-and-using-rix-on-macos.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + *This vignette will discuss macOS-specific topics. If you're not using macOS, you can ignore this vignette, and read the @@ -37,14 +37,13 @@ idiosyncracies. This vignette details these. ## Installing Nix -You can use `{rix}` to generate Nix expressions even if you don't have Nix installed -on your system, but obviously, you need to install Nix if you actually want to -build the defined development environment and use them. Installing (and uninstalling) -Nix is quite simple, thanks to the installer from -[Determinate +You can use `{rix}` to generate Nix expressions even if you don't have Nix +installed on your system, but obviously, you need to install Nix if you actually +want to build the defined development environment and use them. Installing (and +uninstalling) Nix is quite simple, thanks to the installer from [Determinate Systems](https://determinate.systems/posts/determinate-nix-installer), a company -that provides services and tools built on Nix. Simply open a terminal and run the following -line: +that provides services and tools built on Nix. Simply open a terminal and run +the following line: ```{sh eval = FALSE} @@ -53,7 +52,8 @@ curl --proto '=https' --tlsv1.2 -sSf \ sh -s -- install ``` -Once you have Nix installed, you can build the expressions you generate with `{rix}`! +Once you have Nix installed, you can build the expressions you generate with +`{rix}`! ## What if you don't have R already installed? @@ -85,7 +85,8 @@ rix(r_ver = "latest", to generate a `default.nix`, and then use that file to generate an environment with R, `{dplyr}` and `{ggplot2}`. If you need to add packages for your project, rerun the command above, but add the needed packages to `r_pkgs`. This is -detailled in the vignette `vignette("d1-installing-r-packages-in-a-nix-environment")` and +detailled in the vignette +`vignette("d1-installing-r-packages-in-a-nix-environment")` and `vignette("d2-installing-system-tools-and-texlive-packages-in-a-nix-environment")`. diff --git a/vignettes/c-using-rix-to-build-project-specific-environments.Rmd b/vignettes/c-using-rix-to-build-project-specific-environments.Rmd index a3a93371..21297980 100644 --- a/vignettes/c-using-rix-to-build-project-specific-environments.Rmd +++ b/vignettes/c-using-rix-to-build-project-specific-environments.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + ## Project-specific Nix environments diff --git a/vignettes/d1-installing-r-packages-in-a-nix-environment.Rmd b/vignettes/d1-installing-r-packages-in-a-nix-environment.Rmd index 91e4a666..44146e11 100644 --- a/vignettes/d1-installing-r-packages-in-a-nix-environment.Rmd +++ b/vignettes/d1-installing-r-packages-in-a-nix-environment.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + ## Introduction diff --git a/vignettes/d2-installing-system-tools-and-texlive-packages-in-a-nix-environment.Rmd b/vignettes/d2-installing-system-tools-and-texlive-packages-in-a-nix-environment.Rmd index c56d4bf9..17de0148 100644 --- a/vignettes/d2-installing-system-tools-and-texlive-packages-in-a-nix-environment.Rmd +++ b/vignettes/d2-installing-system-tools-and-texlive-packages-in-a-nix-environment.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + ## Introduction diff --git a/vignettes/e-interactive-use.Rmd b/vignettes/e-interactive-use.Rmd index 6eabd073..312a5994 100644 --- a/vignettes/e-interactive-use.Rmd +++ b/vignettes/e-interactive-use.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + ## Introduction diff --git a/vignettes/z-advanced-topic-building-an-environment-for-literate-programming.Rmd b/vignettes/z-advanced-topic-building-an-environment-for-literate-programming.Rmd index 58dde15f..3b7673c0 100644 --- a/vignettes/z-advanced-topic-building-an-environment-for-literate-programming.Rmd +++ b/vignettes/z-advanced-topic-building-an-environment-for-literate-programming.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + ## Introduction diff --git a/vignettes/z-advanced-topic-handling-packages-with-remote-dependencies.Rmd b/vignettes/z-advanced-topic-handling-packages-with-remote-dependencies.Rmd index 57362e4d..8403900d 100644 --- a/vignettes/z-advanced-topic-handling-packages-with-remote-dependencies.Rmd +++ b/vignettes/z-advanced-topic-handling-packages-with-remote-dependencies.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + ## Introduction diff --git a/vignettes/z-advanced-topic-reproducible-analytical-pipelines-with-nix.Rmd b/vignettes/z-advanced-topic-reproducible-analytical-pipelines-with-nix.Rmd index b91034b4..7e59afdc 100644 --- a/vignettes/z-advanced-topic-reproducible-analytical-pipelines-with-nix.Rmd +++ b/vignettes/z-advanced-topic-reproducible-analytical-pipelines-with-nix.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + ## Introduction diff --git a/vignettes/z-advanced-topic-running-r-or-shell-code-in-nix-from-r.Rmd b/vignettes/z-advanced-topic-running-r-or-shell-code-in-nix-from-r.Rmd index 3d6f3454..7b1f1261 100644 --- a/vignettes/z-advanced-topic-running-r-or-shell-code-in-nix-from-r.Rmd +++ b/vignettes/z-advanced-topic-running-r-or-shell-code-in-nix-from-r.Rmd @@ -18,7 +18,7 @@ knitr::opts_chunk$set( library(rix) ``` - + ## **Testing code in evolving software dependency environments with confidence**