R/rix.R

Defines functions rix

Documented in rix

#' Generate a Nix expression that builds a reproducible development environment
#' @return Nothing, this function only has the side-effect of writing two files:
#'   `default.nix` and `.Rprofile` in the working directory. `default.nix`
#'   contains a Nix expression to build a reproducible environment using the Nix
#'   package manager, and `.Rprofile` ensures that a running R session from a
#'   Nix environment cannot access local libraries, nor install packages using
#'   `install.packages()` (nor remove nor update them).
#' @param r_ver Character. The required R version, for example "4.0.0". You can
#'   check which R versions are available using `available_r()`, and for more
#'   details check `available_df()`. For reproducibility purposes, you can also
#'   provide a `nixpkgs` revision directly. For older versions of R, `nix-build`
#'   might fail with an error stating 'this derivation is not meant to be
#'   built'. In this case, simply drop into the shell with `nix-shell` instead
#'   of building it first. It is also possible to provide either "bleeding-edge"
#'   or "frozen-edge" if you need an environment with bleeding edge packages.
#'   Read more in the "Details" section below.
#' @param date Character. Instead of providing `r_ver`, it is also possible to
#'   provide a date. This will build an environment containing R and R packages
#'   (and other dependencies) as of that date. You can check which dates are
#'   available with `available_dates()`. For more details about versions check
#'   `available_df()`.
#' @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. You can look for available software on the NixOS website
#'   \url{https://search.nixos.org/packages?channel=unstable&from=0&size=50&sort=relevance&type=packages&query=}
#' @param git_pkgs List. A list of packages to install from Git. See details for
#'   more information.
#' @param local_r_pkgs Vector of characters, paths to local packages to install.
#'   These packages need to be in the `.tar.gz` or `.zip` formats and must be in
#'   the same folder as the generated "default.nix" file.
#' @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("d2- installing-system-tools-and-texlive-packages-in-a-nix-environment")`
#'   for more details.
#' @param py_conf List. A list containing two elements: `py_version` and
#'   `py_pkgs`. `py_version` should be in the form `"3.12"` for Python 3.12, and
#'   `py_pkgs` should be an atomic vector of package names (e.g., `py_pkgs =
#'   c("polars", "plotnine", "great-tables")`). If Python packages are requested
#'   but `{reticulate}` is not in the list of R packages, the user will be
#'   warned that they may want to add it. When `py_conf` packages are requested,
#'   the `RETICULATE_PYTHON` environment variable is set to ensure the Nix
#'   environment does not use with a system-wide Python installation.
#' @param jl_conf List. A list of two elements, `jl_version` and `jl_conf`.
#'   `jl_version` must be of the form `"1.10"` for Julia 1.10. Leave empty or
#'   use an empty string to use the latest version, or use `"lts"` for the long
#'   term support version. `jl_conf` must be an atomic vector of packages names,
#'   for example `jl_conf = c("TidierData", "TidierPlots")`.
#' @param ide Character, defaults to "none". If you wish to use RStudio to work
#'   interactively use "rstudio" or "rserver" for the server version. Use "code"
#'   for Visual Studio Code or "codium" for Codium, or "positron" for Positron.
#'   You can also use "radian", an interactive REPL. This will install a
#'   project-specific version of the chosen editor which will be differrent than
#'   the one already present in your system (if any). For other editors or if
#'   you want to use an editor already installed on your system (which will
#'   require some configuration to make it work seamlessly with Nix shells see
#'   the `vignette("e-configuring-ide")` for configuration examples), use
#'   "none". Please be aware that VS Code and Positron are not free software. To
#'   facilitate their installation, `rix()` automatically enables a required
#'   setting without prompting the user for confirmation. See the "Details"
#'   section below for more information.
#' @param project_path Character, 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". If the folder does not exist, it will
#'   be created.
#' @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 message_type Character. Message type, defaults to `"simple"`, which
#'   gives minimal but sufficient feedback. Other values are currently
#'   `"quiet`, which generates the files without message, and `"verbose"`, displays all the messages.
#' @param shell_hook Character of length 1, defaults to `NULL`. Commands added
#'   to the `shellHook` variable are executed when the Nix shell starts. So by
#'   default, using `nix-shell default.nix` will start a specific program,
#'   possibly with flags (separated by space), and/or do shell actions. You can
#'   for example use `shell_hook = R`, if you want to directly enter the
#'   declared Nix R session when dropping into the Nix shell.
#' @param ignore_remotes_cache Logical, defaults to FALSE. This variable is only
#'   needed when adding packages from GitHub with remote dependencies, it can be
#'   ignored otherwise. If `TRUE`, the cache of already processed GitHub remotes
#'   will be ignored and all packages will be processed. If `FALSE`, the cache
#'   will be used to skip already processed packages, which makes use of fewer
#'   API calls. Setting this argument to `TRUE` can be useful for debugging.
#' @details This function will write a `default.nix` and an `.Rprofile` 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.
#'
#'   It is possible to use environments built with Nix interactively, either
#'   from the terminal, or using an interface such as RStudio. If you want to
#'   use RStudio, set the `ide` argument to `"rstudio"`. Please be aware that
#'   for macOS, RStudio is only available starting from R version 4.4.3 or from
#'   the 2025-02-28. As such, you may want to use another editor on macOS if you
#'   need to use an environment with an older version of R. To use Visual Studio
#'   Code (or Codium), set the `ide` argument to `"code"` or `"codium"`
#'   respectively, which will add the `{languageserver}` R package to the list
#'   of R packages to be installed by Nix in that environment. It is also
#'   possible to use Positron by setting the `ide` argument to `"positron"`.
#'   Setting the `ide` argument to an editor will install it from Nix, meaning
#'   that each of your projects can have a dedicated IDE (or IDE version).
#'   `"radian"` and `"rserver"` are also options.
#'
#'   Instead of using Nix to install an IDE, you can also simply use the one you
#'   have already installed on your system, with the exception of RStudio which
#'   must be managed by Nix to "see" Nix environments. Positron must also be
#'   heavily configured to work with Nix shells, so we recommend installing it
#'   using Nix. To use an editor that you already have installed on your system,
#'   set `ide = "none"` and refer to the `vignette("e-configuring-ide")` for
#'   more details on how to set up your editor to work with Nix shells.
#'
#'   Packages to install from GitHub or Gitlab must be provided in a list of 3
#'   elements: "package_name", "repo_url" and "commit". To install several
#'   packages, provide a list of lists of these 3 elements, one per package to
#'   install. 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. For security purposes, users that wish to
#'   install packages from GitHub/GitLab or from the CRAN archives must provide
#'   a security hash for each package. `{rix}` automatically precomputes this
#'   hash for the source directory of R packages from GitHub/Gitlab or from the
#'   CRAN archives, to make sure the expected trusted sources that match the
#'   precomputed hashes in the `default.nix` are downloaded, but only if Nix
#'   is installed. If you need to generate an expression with such packages,
#'   but are working on a system where you can't install Nix, consider generating
#'   the expression using a continuous integration service, such as GitHub
#'   Actions.
#'
#'   Note that installing packages from Git or old versions using the `"@"`
#'   notation or local packages, does not leverage Nix's capabilities for
#'   dependency solving. As such, you might have trouble installing these
#'   packages. If that is the case, open an issue on `{rix}`'s GitHub
#'   repository.
#'
#'   If GitHub packages have dependencies on GitHub as well, `{rix}` will
#'   attempt to generate the correct expression, but we highly recommend you
#'   read the
#'   `vignette("z-advanced-topic-handling-packages-with-remote-dependencies")`
#'   Vignette.
#'
#'   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.
#'
#'   It is possible to use `"bleeding-edge`" or `"frozen-edge`" as the value for
#'   the `r_ver` argument. This will create an environment with the very latest
#'   R packages. `"bleeding-edge`" means that every time you will build the
#'   environment, the packages will get updated. This is especially useful for
#'   environments that need to be constantly updated, for example when
#'   developing a package. In contrast, `"frozen-edge`" will create an
#'   environment that will remain stable at build time. So if you create a
#'   `default.nix` file using `"bleeding-edge`", each time you build it using
#'   `nix-build` that environment will be up-to-date. With `"frozen-edge`" that
#'   environment will be up-to-date on the date that the `default.nix` will be
#'   generated, and then each subsequent call to `nix-build` will result in the
#'   same environment. `"bioc-devel"` is the same as `"bleeding-edge"`, but also
#'   adds the development version of Bioconductor. `"r-devel"` is the same as
#'   bleeding edge, but with the R development version instead of the latest
#'   stable version and `"r-devel-bioc-devel"` is the same as `"r-devel"` but
#'   with Bioconductor on the development version. We highly recommend you read
#'   the vignette titled
#'   "z - Advanced topic: Understanding the rPackages set release cycle and
#'   using bleeding edge packages".
#' @export
#' @examples
#' \dontrun{
#' # Build an environment with the latest version of R available from Nixpkgs
#' # and the dplyr and ggplot2 packages
#' rix(
#'   r_ver = "latest-upstream",
#'   r_pkgs = c("dplyr", "ggplot2"),
#'   system_pkgs = NULL,
#'   git_pkgs = NULL,
#'   local_r_pkgs = NULL,
#'   ide = "code",
#'   project_path = path_default_nix,
#'   overwrite = TRUE,
#'   print = TRUE,
#'   message_type = "simple",
#'   shell_hook = NULL,
#'   ignore_remotes_cache = FALSE
#' )
#' }
rix <- function(
  r_ver = NULL,
  date = NULL,
  r_pkgs = NULL,
  system_pkgs = NULL,
  git_pkgs = NULL,
  local_r_pkgs = NULL,
  tex_pkgs = NULL,
  py_conf = NULL,
  jl_conf = NULL,
  ide = "none",
  project_path,
  overwrite = FALSE,
  print = FALSE,
  message_type = "simple",
  shell_hook = NULL,
  ignore_remotes_cache = FALSE
) {
  message_type <- match.arg(
    message_type,
    choices = c("quiet", "simple", "verbose")
  )

  if (ide == "other") {
    stop(
      "ide = 'other' has been deprecated in favour of ide = 'none' as of version 0.15.0."
    )
  } else if (ide == "code") {
    warning(
      "The behaviour of the 'ide' argument changed since version 0.15.0; we highly recommend reading this vignette: https://docs.ropensci.org/rix/articles/e-configuring-ide.html if you want to use VS Code."
    )
  } else if (
    !(ide %in%
      c(
        "none",
        "code",
        "codium",
        "positron",
        "radian",
        "rstudio",
        "rserver"
      ))
  ) {
    stop(
      "'ide' must be one of 'none', 'code', 'codium', 'positron', 'radian', 'rstudio', 'rserver'"
    )
  }

  if (!is.null(date) && !(date %in% available_dates())) {
    # nolint start: line_length_linter
    stop(
      "The provided date is not available.\nRun available_dates() to see which dates are available."
    )
    # nolint end
  }

  if (!is.null(date) && !is.null(r_ver)) {
    stop("Provide either an R version or a date, not both.")
  }

  if (is.null(date) && r_ver == "latest") {
    stop(
      "'latest' was deprecated in favour of 'latest-upstream' as of version 0.14.0."
    )
  }

  if (
    !(message_type %in% c("simple", "quiet")) &&
      r_ver %in%
        c(
          "bleeding-edge",
          "frozen-edge",
          "r-devel",
          "bioc-devel",
          "r-devel-bioc-devel"
        )
  ) {
    warning(
      "You chose 'bleeding-edge', 'frozen-edge', 'r-devel', 'bioc-devel' or 'r-devel-bioc-devel'
as the value for `r_ver`. Please read the vignette
https://docs.ropensci.org/rix/articles/z-bleeding-edge.html
before continuing."
    )
  }

  if (
    identical(ide, "rstudio") &&
      is.null(r_pkgs) &&
      is.null(git_pkgs) &&
      is.null(local_r_pkgs)
  ) {
    stop(
      paste0(
        "You chose 'rstudio' as the IDE, but didn't add any R packages",
        " to the environment.\nThis expression will not build successfully. ",
        "Consider adding R packages."
      )
    )
  }

  if (!is.null(jl_conf) && date < as.Date("2025-09-04")) {
    warning(
      "Julia support is only guaranteed from 2025-09-04 onward.",
      "If environment building fails, try using a later date."
    )
  }

  # Wrapper attributes to be used later
  attrib <- c(
    radian = "radianWrapper",
    rstudio = "rstudioWrapper",
    rserver = "rstudioServerWrapper"
  )

  if (
    message_type != "quiet" &&
      Sys.info()["sysname"] == "Darwin" &&
      ide == "rstudio" &&
      ((r_ver < "4.4.3" && is.null(date)) ||
        (is.null(r_ver) && date < as.Date("2025-02-28")))
  ) {
    warning(
      "Your operating system is detected as macOS, but you selected 'rstudio'
for an R version or date that does not support it.
To use RStudio on macOS, select at least R 4.4.3 or a
date on or after 2025-02-28. If you require an older R version or date,
choose a different IDE for compatibility.
Please refer to the macOS-specific vignette
https://docs.ropensci.org/rix/articles/b2-setting-up-and-using-rix-on-macos.html
for more details."
    )
  }

  if (isFALSE(dir.exists(project_path))) {
    dir.create(path = project_path, recursive = TRUE)
    project_path <- normalizePath(path = project_path)
  }

  # nolint start: object_name_linter
  default.nix_path <- file.path(project_path, "default.nix")
  .Rprofile_path <- file.path(project_path, ".Rprofile")
  # nolint end

  # Find url to use
  # In all cases but 'latest-upstream', the rstats-on-nix/nixpkgs
  # fork is used. Otherwise, upstream NixOS/nixpkgs
  nix_repo <- make_nixpkgs_url(r_ver, date)

  rix_call <- match.call()

  # Get the two lists. One list is current CRAN packages
  # the other is archived CRAN packages.
  cran_pkgs <- get_rpkgs(r_pkgs, ide)

  # If there are R packages, passes the string "rpkgs" to buildInputs
  flag_rpkgs <- if (is.null(cran_pkgs$rPackages) || cran_pkgs$rPackages == "") {
    ""
  } else {
    "rpkgs"
  }

  # If there are LaTeX packages, passes the string "tex" to buildInputs
  flag_tex_pkgs <- if (is.null(tex_pkgs)) {
    ""
  } else {
    "tex"
  }

  # If there are R packages from Git, passes the string "git_archive_pkgs" to buildInputs
  flag_git_archive <- if (
    !is.null(git_pkgs) || !is.null(cran_pkgs$archive_pkgs)
  ) {
    # If git_pkgs is a list of lists, then sapply will succeed
    # if not, then we can access "package_name" directly
    git_pkgs_names <- if (!is.null(git_pkgs)) {
      tryCatch(
        sapply(git_pkgs, function(x) x$package_name),
        error = function(e) git_pkgs$package_name
      )
    }
    # CRAN archive pkgs are written as "AER@123"
    # so we need to split at the '@' character and then
    # walk through the list to grab the first element
    # which will be the name of the package
    cran_archive_names <- if (!is.null(cran_pkgs$archive_pkgs)) {
      pkgs <- strsplit(cran_pkgs$archive_pkgs, split = "@")
      sapply(pkgs, function(x) x[[1]])
    }

    paste0(c(git_pkgs_names, cran_archive_names), collapse = " ")
  } else {
    ""
  }

  # If there are local R packages, passes the string "local_r_pkgs" to buildInputs
  flag_local_r_pkgs <- if (is.null(local_r_pkgs)) {
    ""
  } else {
    "local_r_pkgs"
  }

  # If there are Python packages, passes the string "pyconf" to buildInputs

  if (!is.null(py_conf)) {
    if (
      !any(grepl("reticulate", c(cran_pkgs$rPackages, cran_pkgs$archive_pkgs)))
    ) {
      warning(
        "Python packages have been requested, but 'reticulate' is not in your list of R packages. ",
        "If you want to handle Python objects from your R session, consider adding 'reticulate' to the list of R packages."
      )
    }
    flag_py_conf <- "pyconf"
  } else {
    flag_py_conf <- ""
  }

  if (!is.null(jl_conf)) {
    flag_jl_conf <- "jlconf"
  } else {
    flag_jl_conf <- ""
  }

  # If there are wrapped packages (for example for RStudio), passes the "wrapped_pkgs"
  # to buildInputs
  flag_wrapper <- if (ide %in% names(attrib) && flag_rpkgs != "") {
    "wrapped_pkgs"
  } else {
    ""
  }

  # Correctly formats shellHook for Nix's mkShell
  shell_hook <- if (!is.null(shell_hook) && nzchar(shell_hook)) {
    paste0('shellHook = "', shell_hook, '";')
  } else {
    ""
  }

  default.nix <- paste(
    generate_header(
      nix_repo,
      r_ver,
      rix_call,
      ide
    ),
    generate_rpkgs(cran_pkgs$rPackages, flag_rpkgs),
    generate_git_archived_pkgs(
      git_pkgs,
      cran_pkgs$archive_pkgs,
      flag_git_archive,
      ignore_remotes_cache = ignore_remotes_cache
    ),
    generate_tex_pkgs(tex_pkgs),
    generate_py_conf(py_conf, flag_py_conf),
    generate_jl_conf(jl_conf, flag_jl_conf),
    generate_local_r_pkgs(local_r_pkgs, flag_local_r_pkgs),
    generate_system_pkgs(system_pkgs, r_pkgs, py_conf, ide),
    generate_wrapped_pkgs(
      ide,
      attrib,
      flag_git_archive,
      flag_rpkgs,
      flag_local_r_pkgs
    ),
    generate_shell(
      flag_git_archive,
      flag_rpkgs,
      flag_tex_pkgs,
      py_conf,
      flag_py_conf,
      flag_jl_conf,
      flag_local_r_pkgs,
      flag_wrapper,
      shell_hook
    ),
    generate_inherit(),
    collapse = "\n"
  )

  # Generate default.nix file # nolint next: object_name_linter
  default.nix <- strsplit(default.nix, split = "\n")[[1]]

  # Remove consecutive empty lines
  default.nix <- remove_empty_lines(default.nix)

  if (print) {
    print(default.nix)
  }

  if (!file.exists(default.nix_path) || overwrite) {
    if (!dir.exists(project_path)) {
      dir.create(project_path, recursive = TRUE)
    }
    con <- file(default.nix_path, open = "wb", encoding = "native.enc")
    on.exit(close(con))

    writeLines(enc2utf8(default.nix), con = con, useBytes = TRUE)

    if (file.exists(.Rprofile_path)) {
      if (
        !any(grepl(
          "File generated by `rix::rix_init()",
          readLines(.Rprofile_path)
        ))
      ) {
        if (
          message_type != "quiet" && identical(Sys.getenv("TESTTHAT"), "false")
        ) {
          message("\n\n### Successfully generated `default.nix` ###\n\n")
        }
        warning(
          "\n\n### .Rprofile file already exists. ",
          "You may want to call rix_init(rprofile_action = 'append') manually ",
          "to ensure correct functioning of your Nix environment. ###\n\n"
        )
      } else {
        if (
          message_type != "quiet" && identical(Sys.getenv("TESTTHAT"), "false")
        ) {
          message(
            sprintf(
              "\n\n### Successfully generated `default.nix` in %s. ",
              normalizePath(project_path)
            ),
            "Keeping `.Rprofile` generated by `rix::rix_init()`###\n\n"
          )
        }
      }
    } else if (!file.exists(.Rprofile_path)) {
      rix_init(
        project_path = project_path,
        rprofile_action = "create_missing",
        # 'verbose' is too chatty for rix()
        # hence why it's transformed to "simple"
        message_type = ifelse(message_type == "verbose", "simple", message_type)
      )

      if (
        message_type != "quiet" && identical(Sys.getenv("TESTTHAT"), "false")
      ) {
        message(
          "\n\n### Successfully generated `default.nix` and `.Rprofile` ###\n\n"
        )
      }
    }
  } else {
    project_path <- if (project_path == ".") {
      "current folder"
    } else {
      project_path
    }
    stop(
      paste0(
        "`default.nix` exists in ",
        project_path,
        ". Set `overwrite == TRUE` to overwrite."
      )
    )
  }
  on.exit(close(con))
}

Try the rix package in your browser

Any scripts or data that you put into this service are public.

rix documentation built on Sept. 11, 2025, 5:12 p.m.