R/conda.R

Defines functions local_conda_paths conda_bin_paths conda_prefix get_python_conda_info is_conda_python conda_info conda_run2_nix conda_run2_windows conda_run2 conda_run conda_installed conda_list_packages is_condaenv conda_args condaenv_exists condaenv_path condaenv_resolve find_conda conda_search conda_python numeric_conda_version conda_update conda_version conda_binary conda_install conda_remove conda_export conda_clone conda_create_env conda_create conda_list

Documented in conda_binary conda_clone conda_create condaenv_exists conda_export conda_install conda_list conda_python conda_remove conda_run2 conda_search conda_update conda_version

#' Conda Tools
#'
#' Tools for managing Python `conda` environments.
#'
#' @param envname The name of, or path to, a conda environment.
#'
#' @param conda The path to a `conda` executable. Use `"auto"` to allow
#'   `reticulate` to automatically find an appropriate `conda` binary.
#'   See **Finding Conda** and [conda_binary()] for more details.
#'
#' @param forge Boolean; include the [conda-forge](https://conda-forge.org/)
#'   repository?
#'
#' @param channel An optional character vector of conda channels to include.
#'   When specified, the `forge` argument is ignored. If you need to
#'   specify multiple channels, including the conda forge, you can use
#'   `c("conda-forge", <other channels>)`.
#'
#' @param packages A character vector, indicating package names which should be
#'   installed or removed. Use  \verb{<package>==<version>} to request the installation
#'   of a specific version of a package. A `NULL` value for [conda_remove()]
#'   will be interpretted to `"--all"`, removing the entire environment.
#'
#' @param environment The path to an environment definition, generated via
#'   (for example) [conda_export()], or via `conda env export`. When provided,
#'   the conda environment will be created using this environment definition,
#'   and other arguments will be ignored.
#'
#' @param python_version The version of Python to be used in this conda
#'   environment. The associated Python package from conda will be requested
#'   as `python={python_version}`. When `NULL`, the default `python` package
#'   will be used instead. For example, use `python_version = "3.6"` to request
#'   that the conda environment be created with a copy of Python 3.6. This
#'   argument will be ignored if `python` is specified as part of the `packages`
#'   argument, for backwards compatibility.
#'
#' @param file The path where the conda environment definition will be written.
#'
#' @param json Boolean; should the environment definition be written as JSON?
#'   By default, conda exports environments as YAML.
#'
#' @param pip Boolean; use `pip` for package installation? By default, packages
#'   are installed from the active conda channels.
#'
#' @param pip_ignore_installed Ignore already-installed versions when using pip?
#'   (defaults to `FALSE`). Set this to `TRUE` so that specific package versions
#'   can be installed even if they are downgrades. The `FALSE` option is useful
#'   for situations where you don't want a pip install to attempt an overwrite of
#'   a conda binary package (e.g. SciPy on Windows which is very difficult to
#'   install via pip due to compilation requirements).
#'
#' @param pip_options An optional character vector of additional command line
#'   arguments to be passed to `pip`. Only relevant when `pip = TRUE`.
#'
#' @param python_version The version of Python to be installed. Set this if
#'   you'd like to change the version of Python associated with a particular
#'   conda environment.
#'
#' @param clone The name of the conda environment to be cloned.
#'
#' @param all Boolean; report all instances of Python found?
#'
#' @param additional_create_args An optional character vector of additional
#'   arguments to use in the call to `conda create`.
#'
#' @param additional_install_args An optional character vector of additional
#'   arguments to use in the call to `conda install`.
#'
#' @param ... Optional arguments, reserved for future expansion.
#'
#'
#' @section Finding Conda:
#'
#' Most of `reticulate`'s conda APIs accept a `conda` parameter, used to control
#' the `conda` binary used in their operation. When `conda = "auto"`,
#' `reticulate` will attempt to automatically find a conda installation.
#' The following locations are searched, in order:
#'
#' 1. The location specified by the `reticulate.conda_binary` \R option,
#' 2. The location specified by the `RETICULATE_CONDA` environment variable,
#' 3. The [miniconda_path()] location (if it exists),
#' 4. The program `PATH`,
#' 5. A set of pre-defined locations where conda is typically installed.
#'
#' To force `reticulate` to use a particular `conda` binary, we recommend
#' setting:
#'
#' ```r
#' options(reticulate.conda_binary = "/path/to/conda")
#' ```
#'
#' This can be useful if your conda installation lives in a location that
#' `reticulate` is unable to automatically discover.
#'
#' @name conda-tools
#' @seealso [conda_run2()]
NULL


#' @returns
#'   `conda_list()` returns an \R `data.frame`, with `name` giving the name of
#'   the associated environment, and `python` giving the path to the Python
#'   binary associated with that environment.
#'
#' @rdname conda-tools
#' @export
conda_list <- function(conda = "auto") {

  # resolve conda binary
  conda <- conda_binary(conda)
  local_conda_paths(conda)

  if (startsWith(basename(conda), "micromamba")) {
    conda_envs <- system2(conda, c("env", "list", "--json"),
                          stdout = TRUE, stderr = FALSE)

  } else {
  # list envs -- discard stderr as Anaconda may emit warnings that can
  # otherwise be ignored; see e.g. https://github.com/rstudio/reticulate/issues/474
    conda_envs <- suppressWarnings(
      system2(conda, args = c("info", "--json"),
              stdout = TRUE, stderr = FALSE)
    )
  }

  # check for error
  status <- attr(conda_envs, "status") %||% 0L
  if (status != 0L) {

    # show warning if conda_diagnostics are enabled
    if (getOption("reticulate.conda_diagnostics", default = FALSE)) {
      errmsg <- attr(status, "errmsg")
      warning("Error ", status, " occurred running ", conda, " ", errmsg)
    }

    # return empty data frame
    return(data.frame(
      name = character(),
      python = character(),
      stringsAsFactors = FALSE)
    )

  }

  # strip out anaconda cloud prefix (not valid json)
  if (length(conda_envs) > 0 && grepl("Anaconda Cloud", conda_envs[[1]], fixed = TRUE))
    conda_envs <- conda_envs[-1]

  # parse conda info
  info <- fromJSON(conda_envs)

  # convert to json
  conda_envs <- info$envs

  # normalize and remove duplicates (seems necessary on Windows as Anaconda
  # may report both short-path and long-path versions of the same environment)
  conda_envs <- unique(normalizePath(conda_envs, mustWork = FALSE))

  # return an empty data.frame when no envs are found
  if (length(conda_envs) == 0L) {
    return(data.frame(
      name = character(),
      python = character(),
      stringsAsFactors = FALSE)
    )
  }

  # build data frame
  name <- basename(conda_envs)

  suffix <- if (is_windows()) "python.exe" else "bin/python"
  python <- paste(conda_envs, suffix, sep = "/")

  # handle base environment specially
  # (compare normalized paths for Windows's sake)
  prefix <- info$root_prefix %||% ""
  isbase <-
    normalizePath(prefix, winslash = "/", mustWork = FALSE) ==
    normalizePath(conda_envs, winslash = "/", mustWork = FALSE)

  name[isbase] <- "base"

  data.frame(
    name = name,
    python = python,
    stringsAsFactors = FALSE
  )

}

#' @returns
#'   `conda_create()` returns the path to the Python binary associated with the
#'   newly-created conda environment.
#'
#' @rdname conda-tools
#' @export
conda_create <- function(envname = NULL,
                         packages = NULL,
                         ...,
                         forge = TRUE,
                         channel = character(),
                         environment = NULL,
                         conda = "auto",
                         python_version = miniconda_python_version(),
                         additional_create_args = character())
{
  check_forbidden_install("Conda Environments")

  # resolve conda binary
  conda <- conda_binary(conda)
  local_conda_paths(conda)

  # if environment is provided, use it directly
  if (!is.null(environment))
    return(conda_create_env(envname, environment, conda, additional_create_args))

  # resolve environment name
  envname <- condaenv_resolve(envname)

  # resolve packages argument
  if (!any(grepl("^python", packages))) {

    python_package <- if (is.null(python_version))
      "python"
    else
      sprintf("python=%s", python_version)

    packages <- c(python_package, packages)

  }

  # create the environment
  args <- conda_args("create", envname, packages)

  # be quiet
  args <- c(args, "--quiet")

  # add user-requested channels
  channels <- if (length(channel))
    channel
  else if (forge)
    "conda-forge"

  for (ch in channels)
    args <- c(args, "-c", ch)

  # add additional arguments
  args <- c(args, additional_create_args)

  # invoke conda
  result <- system2t(conda, maybe_shQuote(args))
  if (result != 0L) {
    fmt <- "Error creating conda environment '%s' [exit code %i]"
    stopf(fmt, envname, result, call. = FALSE)
  }

  # return the path to the python binary
  conda_python(envname = envname, conda = conda)
}

conda_create_env <- function(envname, environment, conda, additional_create_args) {

  check_forbidden_install("Conda Environments")

  if (!is.null(envname))
    envname <- condaenv_resolve(envname)

  args <- c(
    "env", "create", "--quiet",
    if (is.null(envname))
      c()
    else if (grepl("/", envname))
      c("--prefix", maybe_shQuote(envname))
    else
      c("--name", maybe_shQuote(envname)),
    "-f", maybe_shQuote(environment),
    additional_create_args
  )

  result <- system2t(conda, args)
  if (result != 0L) {
    fmt <- "Error creating conda environment [exit code %i]"
    stopf(fmt, result)
  }

  # return the path to the python binary
  conda_python(envname = envname, conda = conda)

}

#' @returns
#'   `conda_clone()` returns the path to Python within the newly-created
#'   conda environment.
#'
#' @rdname conda-tools
#' @export
conda_clone <- function(envname, ..., clone = "base", conda = "auto") {

  # resolve conda binary
  conda <- conda_binary(conda)
  local_conda_paths(conda)

  # resolve environment name
  envname <- condaenv_resolve(envname)

  # create the environment
  args <- conda_args("create", envname)

  # be quiet
  args <- c(args, "--quiet")

  # add cloned environment
  args <- c(args, "--clone", clone)

  # invoke conda
  result <- system2t(conda, maybe_shQuote(args))
  if (result != 0L) {
    fmt <- "Error creating conda environment '%s' [exit code %i]"
    stopf(fmt, envname, result, call. = FALSE)
  }

  # return the path to the python binary
  conda_python(envname = envname, conda = conda)

}

#' @returns
#'   `conda_export()` returns the path to the exported environment definition,
#'   invisibly.
#'
#' @rdname conda-tools
#' @export
conda_export <- function(envname,
                         file = if (json) "environment.json" else "environment.yml",
                         json = FALSE,
                         ...,
                         conda = "auto")
{
  # resolve parameters
  conda <- conda_binary(conda)
  local_conda_paths(conda)
  envname <- condaenv_resolve(envname)

  # build conda argument list,
  args <- c(
    "env", "export",
    if (json)
      "--json",
    if (grepl("/", envname))
      c("--prefix", shQuote(envname))
    else
      c("--name", shQuote(envname)),
    ">", shQuote(file)
  )

  # execute conda
  status <- system2(conda, args)
  if (status != 0L) {
    fmt <- "Error exporting conda environment [error code %i]"
    stopf(fmt, status, call. = FALSE)
  }

  # notify user
  fmt <- "* Environment '%s' exported to '%s'."
  writeLines(sprintf(fmt, envname, file))

  # return path to generated environment
  invisible(file)
}

#' @rdname conda-tools
#' @export
conda_remove <- function(envname,
                         packages = NULL,
                         conda = "auto")
{
  # resolve conda binary
  conda <- conda_binary(conda)
  local_conda_paths(conda)

  # resolve environment name
  envname <- condaenv_resolve(envname)

  # no packages means everything
  if (is.null(packages))
    packages <- "--all"

  # remove packages (or the entire environment)
  args <- conda_args("remove", envname, packages)
  result <- system2t(conda, maybe_shQuote(args))
  if (result != 0L) {
    stop("Error ", result, " occurred removing conda environment ", envname,
         call. = FALSE)
  }
}

#' @rdname conda-tools
#' @export
conda_install <- function(envname = NULL,
                          packages,
                          forge = TRUE,
                          channel = character(),
                          pip = FALSE,
                          pip_options = character(),
                          pip_ignore_installed = FALSE,
                          conda = "auto",
                          python_version = NULL,
                          additional_create_args = character(),
                          additional_install_args = character(),
                          ...)
{
  check_forbidden_install("Python packages")

  # check that 'packages' argument was supplied
  if (missing(packages)) {
    if (!is.null(envname)) {

      fmt <- paste(
        "argument \"packages\" is missing, with no default",
        "- did you mean 'conda_install(<envname>, %1$s)'?",
        "- use 'py_install(%1$s)' to install into the active Python environment",
        sep = "\n"
      )

      stopf(fmt, deparse1(substitute(envname)), call. = FALSE)
    } else {
      packages
    }
  }

  # resolve conda binary
  conda <- conda_binary(conda)
  local_conda_paths(conda)

  # resolve environment name
  envname <- condaenv_resolve(envname)

  # honor request for specific version of Python package
  python_package <- if (is.null(python_version))
    NULL
  else if (grepl("[><=]", python_version))
    paste0("python", python_version)
  else
    sprintf("python=%s", python_version)

  # check to see if we already have a valid Python installation for
  # this conda environment
  python <- tryCatch(
    conda_python(envname = envname, conda = conda),
    error = identity
  )

  # if this conda environment doesn't seem to exist, auto-create it
  if (inherits(python, "error") || !file.exists(python)) {

    conda_create(
      envname = envname,
      packages = python_package %||% "python",
      forge = forge,
      channel = channel,
      conda = conda,
      additional_create_args = additional_create_args
    )

    python <- conda_python(envname = envname, conda = conda)

  }

  # prepare user-requested channels
  channels <- if (length(channel))
    channel
  else if (forge)
    "conda-forge"

  channel_args <- character()
  for (ch in channels)
    channel_args <- c(channel_args, "-c", ch)

  # if the user has requested a specific version of Python, ensure that
  # version of Python is installed into the requested environment
  # (should be no-op if that copy of Python already installed)
  if (!is.null(python_version)) {
    args <- conda_args("install", envname, python_package)
    args <- c(args, channel_args, additional_install_args)
    status <- system2t(conda, maybe_shQuote(args))
    if (status != 0L) {
      fmt <- "installation of '%s' into environment '%s' failed [error code %i]"
      msg <- sprintf(fmt, python_package, envname, status)
      stop(msg, call. = FALSE)
    }
  }

  # delegate to pip if requested
  if (pip) {

    result <- pip_install(
      python = python,
      packages = packages,
      pip_options = pip_options,
      ignore_installed = pip_ignore_installed,
      conda = conda,
      envname = envname
    )

    return(result)

  }

  # otherwise, use conda
  args <- conda_args("install", envname)

  # add user-requested channels
  args <- c(args, channel_args)

  args <- c(args, python_package, packages, additional_install_args)
  result <- system2t(conda, maybe_shQuote(args))

  # check for errors
  if (result != 0L) {
    fmt <- "one or more Python packages failed to install [error code %i]"
    stopf(fmt, result)
  }


  invisible(packages)
}

#' @rdname conda-tools
#' @export
conda_binary <- function(conda = "auto") {

  # automatic lookup if requested
  if (identical(conda, "auto") || isTRUE(is.na(conda))) {
    conda <- find_conda()
    if (is.null(conda))
      stop("Unable to find conda binary. Is Anaconda installed?", call. = FALSE)
    conda <- conda[[1]]
  }

  conda <- normalizePath(conda, winslash = "/", mustWork = FALSE)
  if (!grepl("^(conda|mamba|micromamba)", basename(conda)))
    warning("Supplied path is not a conda binary: ", sQuote(conda))

  # if the user has requested a conda binary in the 'condabin' folder,
  # try to find and use its sibling in the 'bin' folder instead as
  # we rely on other tools typically bundled in the 'bin' folder
  # https://github.com/rstudio/keras/issues/691
  if (!is_windows()) {
    altpath <- file.path(dirname(conda), "../bin", basename(conda))
    if (file.exists(altpath))
      return(normalizePath(altpath, winslash = "/", mustWork = TRUE))
  } else {
    # on Windows it's preferable to conda.bat located in the 'condabin'
    # folder. if the user passed the path to a 'Scripts/conda.exe' we will
    # try to find the 'conda.bat'.
    altpath <- file.path(dirname(conda), "../condabin/conda.bat")
    if (file.exists(altpath))
      return(normalizePath(altpath, winslash = "/", mustWork = TRUE))
  }

  # validate existence
  if (!file.exists(conda))
    stop("Specified conda binary '", conda, "' does not exist.", call. = FALSE)

  # return conda
  conda
}

#' @rdname conda-tools
#' @export
conda_exe <- conda_binary


#' @rdname conda-tools
#' @export
conda_version <- function(conda = "auto") {
  conda_bin <- conda_binary(conda)
  local_conda_paths(conda_bin)
  out <- system2(conda_bin, "--version", stdout = TRUE)

  # mamba --version gives multi-line output, with the conda version on the last line.
  tail(out, 1)
}


#' @rdname conda-tools
#' @export
conda_update <- function(conda = "auto") {

  # resolve conda
  conda <- conda_binary(conda)
  local_conda_paths(conda)

  # compute base path
  prefix <- system2(conda, c("info", "--base"), stdout = TRUE)

  # figure out if this is anaconda or 'plain' conda
  json <- system2(conda, c("list", "--prefix", shQuote(prefix), "--json"), stdout = TRUE)
  envlist <- jsonlite::fromJSON(json)
  name <- if ("anaconda" %in% envlist$name) "anaconda" else "conda"

  # attempt update
  system2t(conda, c("update", "--prefix", maybe_shQuote(prefix), "--yes", name))
}

numeric_conda_version <- function(conda = "auto", version_string = conda_version(conda)) {
  # some plausible version strings:
  # "conda 4.6.0"
  # "conda 4.6.0b0"
  # "conda 4.6.0rc1"
  # "conda 4.6.0rc1.post3+64bde06"
  v <- version_string
  v <- sub("^conda ", "", v) # drop hardcoded prefix

  # https://github.com/conda/conda/blob/c1579681d1468af3d1b4af3083bed33f8391e861/conda/_vendor/auxlib/packaging.py#L142
  # if dev version string: "{0}.post{1}+{2}".format(version, post_commit, hash)
  v <- sub("\\.post(\\d)\\+.+$", ".\\1", v)

  # substitute rc|beta|alpha|whatever suffix with .
  v <- sub("[A-Za-z]+", ".", v)

  if (grepl("\\.$", v))
    v <- paste0(v, "9000")

  numeric_version(v)
}


#' @rdname conda-tools
#' @export
conda_python <- function(envname = NULL,
                         conda = "auto",
                         all = FALSE)
{
  # resolve envname
  envname <- condaenv_resolve(envname)

  # for fully-qualified paths, construct path explicitly
  if (grepl("[/\\\\]", envname)) {
    suffix <- if (is_windows()) "python.exe" else "bin/python"
    path <- file.path(envname, suffix)
    if (file.exists(path))
      return(path)

    fmt <- "no conda environment exists at path '%s'"
    stop(sprintf(fmt, envname))
  }

  # otherwise, list conda environments and try to find it
  conda_envs <- conda_list(conda = conda)
  env <- subset(conda_envs, conda_envs$name == envname)
  if (nrow(env) == 0)
    stop("conda environment '", envname, "' not found")

  # NOTE: it's possible to have multiple environments with the same name;
  # e.g. because the user might've installed multiple copies of Anaconda
  # (e.g. both miniconda and miniforge). we should think about having a way
  # of disambiguating this, but for now just pick the first one
  python <- if (all) env$python else env$python[[1L]]
  path.expand(python)
}

#' @returns
#'   `conda_search()` returns an \R `data.frame` describing packages that
#'   matched against `matchspec`. The data frame will usually include
#'   fields `name` giving the package name, `version` giving the package
#'   version, `build` giving the package build, and `channel` giving the
#'   channel the package is hosted on.
#'
#' @param matchspec A conda MatchSpec query string.
#'
#' @rdname conda-tools
#' @export
conda_search <-
  function(matchspec,
           forge = TRUE,
           channel = character(),
           conda = "auto",
           ...) {

  conda <- conda_binary(conda)
  local_conda_paths(conda)

  args <- c("search", matchspec)

  # add user-requested channels
  channels <- if (length(channel))
    channel
  else if (forge)
    "conda-forge"

  for (ch in channels)
    args <- c(args, "-c", ch)

  args = c(args, "--json")
  output <- system2(conda, shQuote(args), stdout = TRUE)
  status <- attr(output, "status") %||% 0L
  # check for errors
  if (status != 0L) {
    fmt <- "error searching for packages [error code %i]"
    stopf(fmt, status)
  }

  parsed <- jsonlite::fromJSON(output)

  all_colnames <- unique(unlist(lapply(parsed, names)))
  df <- do.call(rbind, lapply(parsed, function(x) {
    if(any(missing_cols <- setdiff(all_colnames, names(x))))
      x[missing_cols] <- NA
    x
  }))
  rownames(df) <- NULL

  # reverse row order, so most recent versions are (likely) first. In an ideal
  # world we'd use `order(numeric_version(df$version))`, but we can't depend on
  # the version string being parseable.
  if(nrow(df))
    df <- df[rev(seq_len(nrow(df))), ]

  # reorder columns
  to_front <- intersect(c("name", "version", "build", "channel"), names(df))
  df <- df[c(unique(to_front, names(df)))]

  df
}


find_conda <- function() {

  # allow specification of conda executable
  conda <- getOption("reticulate.conda_binary")
  if (!is.null(conda))
    return(conda)

  conda <- Sys.getenv("RETICULATE_CONDA", unset = NA)
  if (!is.na(conda))
    return(conda)

  # if miniconda is installed, use it
  if (miniconda_exists())
    return(miniconda_conda())

  # if there is a conda executable on the PATH, use it
  conda <- Sys.which("conda")
  if (nzchar(conda))
    return(conda)

  # otherwise, search common locations for conda
  prefixes <- c("~/opt/", "~/", "/opt/", "/")
  names <- c("anaconda", "miniconda", "miniforge")
  versions <- c("", "2", "3", "4")
  combos <- expand.grid(versions, names, prefixes, KEEP.OUT.ATTRS = FALSE, stringsAsFactors = FALSE)
  combos <- combos[rev(seq_along(combos))]
  conda_locations <- unlist(.mapply(paste0, combos, NULL))

  # find the potential conda binary path in each case
  conda_locations <- if (is_windows()) {
    paste0(conda_locations, "/condabin/conda.bat")
  } else {
    paste0(conda_locations, "/bin/conda")
  }

  # look for caskroom version (why does it live in a 'base' subdir?)
  caskroom_conda <- "/opt/homebrew/Caskroom/miniforge/base/bin/conda"
  if (file.exists(caskroom_conda))
    conda_locations <- c(conda_locations, caskroom_conda)

  # ensure we expand tilde prefixes
  conda_locations <- path.expand(conda_locations)

  # on Windows, check the registry for a compatible version of Anaconda
  if (is_windows()) {
    anaconda_versions <- windows_registry_anaconda_versions()
    anaconda_versions <- subset(anaconda_versions, anaconda_versions$arch == .Platform$r_arch)
    if (nrow(anaconda_versions) > 0) {

      conda_scripts <- utils::shortPathName(
        file.path(anaconda_versions$install_path, "Scripts", "conda.exe")
      )
      conda_bats <- utils::shortPathName(
        file.path(anaconda_versions$install_path, "condabin", "conda.bat")
      )

      # prefer versions found in the registry to those found in default locations
      conda_locations <- c(conda_bats, conda_scripts, conda_locations)

    }
  }

  # keep only conda locations that exist
  conda_locations <- conda_locations[file.exists(conda_locations)]
  if (length(conda_locations))
    return(conda_locations)

  # explicitly return NULL when no conda found
  NULL

}

condaenv_resolve <- function(envname = NULL) {

  python_environment_resolve(
    envname = envname,
    resolve = identity
  )

}

condaenv_path <- function(envname = NULL) {

  python_environment_resolve(
    envname = envname,
    resolve = function(name) {
      python <- conda_python(name)
      info <- python_info(python)
      info$root
    }
  )

}

#' @export
#' @rdname conda-tools
condaenv_exists <- function(envname = NULL, conda = "auto") {

  # check that conda is installed
  condabin <- tryCatch(conda_binary(conda = conda), error = identity)
  if (inherits(condabin, "error"))
    return(FALSE)

  # check that the environment exists
  python <- tryCatch(conda_python(envname, conda = conda), error = identity)
  if (inherits(python, "error"))
    return(FALSE)

  # validate the Python binary exists
  file.exists(python)

}

conda_args <- function(action, envname = NULL, ...) {

  envname <- condaenv_resolve(envname)

  # use '--prefix' as opposed to '--name' if envname looks like a path
  args <- c(action, "--yes")
  if (grepl("[/\\]", envname))
    args <- c(args, "--prefix", envname, ...)
  else
    args <- c(args, "--name", envname, ...)

  args

}

is_condaenv <- function(dir) {
  file.exists(file.path(dir, "conda-meta"))
}

conda_list_packages <- function(envname = NULL, conda = "auto", no_pip = TRUE) {

  conda <- conda_binary(conda)
  local_conda_paths(conda)
  envname <- condaenv_resolve(envname)

  # create the environment
  args <- c("list")
  if (grepl("[/\\]", envname)) {
    args <- c(args, "--prefix", envname)
  } else {
    args <- c(args, "--name", envname)
  }

  if (no_pip)
    args <- c(args, "--no-pip")

  args <- c(args, "--json")

  output <- system2(conda, shQuote(args), stdout = TRUE)
  status <- attr(output, "status") %||% 0L
  if (status != 0L) {
    fmt <- "error listing conda environment [status code %i]"
    stopf(fmt, status)
  }

  parsed <- jsonlite::fromJSON(output)

  data.frame(
    package     = parsed$name,
    version     = parsed$version,
    requirement = paste(parsed$name, parsed$version, sep = "="),
    channel     = parsed$channel,
    stringsAsFactors = FALSE
  )

}

conda_installed <- function() {
  condabin <- tryCatch(conda_binary(), error = identity)
  if (inherits(condabin, "error"))
    return(FALSE)
  else
    return(TRUE)
}


conda_run <- function(cmd, args = c(), conda = "auto", envname = NULL,
                      run_args = c("--no-capture-output"), ...) {
  # `conda run` is very unreliable, best avoided. known issues:
  #  - fails if user doesn't have write permissions to conda installation
  #  - fails if arguments need to be quoted

  conda <- conda_binary(conda)
  local_conda_paths(conda)
  envname <- condaenv_resolve(envname)

  if (numeric_conda_version(conda) < "4.9")
    stopf(
"`conda_run()` requires conda version >= 4.9.
Run `miniconda_update('%s')` to update conda.", conda)


  if (grepl("[/\\]", envname))
    in_env <- c("--prefix", shQuote(normalizePath(envname)))
  else
    in_env <- c("--name", envname)

  system2t(conda, c("run", in_env, run_args,
                   shQuote(cmd), args), ...)
}

#' Run a command in a conda environment
#'
#' This function runs a command in a chosen conda environment.
#'
#' Note that, whilst the syntax is similar to [`system2()`], the function
#' dynamically generates a shell script with commands to activate the chosen
#' conda environent. This avoids issues with quoting, as discussed in this
#' [GitHub issue](https://github.com/conda/conda/issues/10972).
#'
#' @param cmd The system command to be invoked, as a character string.
#' @param args A character vector of arguments to the command. The arguments should
#'   be quoted e.g. by `shQuote()` in case they contain space or other special
#'   characters (a double quote or backslash on Windows, shell-specific special
#'   characters on Unix).
#' @param cmd_line The command line to be executed, as a character string. This
#'   is automatically generated from `cmd` and `args`, but can be provided
#'   directly if needed (if provided, it overrides `cmd` and `args`).
#' @param conda The path to a `conda` executable. Use `"auto"` to allow
#'   `reticulate` to automatically find an appropriate `conda` binary.
#'   See **Finding Conda** and [conda_binary()] for more details.
#' @param envname The name of, or path to, a conda environment.
#' @param intern A logical (not `NA`) which indicates whether to capture the
#'   output of the command as an R character vector. If `FALSE` (the default), the
#'   return value is the error code (`0` for success).
#' @param echo A logical (not `NA`) which indicates whether to echo the command to
#'   the console before running it.
#'
#'
#' @returns
#' `conda_run2()` runs a command in the desired conda environment. If
#' `intern = TRUE` the output is returned as a character vector; if `intern = FALSE` (the
#' deafult), then the return value is the error code (0 for success). See
#' [shell()] (on windows) or [`system2()`] on macOS or Linux for more details.
#'
#' @export
#' @seealso [`conda-tools`]
# executes a cmd with a conda env active, implemented directly to avoid using `conda run`
# https://github.com/conda/conda/issues/10972
conda_run2 <- function(cmd, args = c(), conda = "auto", envname = NULL,
                       cmd_line = paste(shQuote(cmd), paste(args, collapse = " ")),
                       intern = FALSE, echo = !intern) {
  run <- if (is_windows()) conda_run2_windows else conda_run2_nix
  run(
    conda = conda,
    envname = envname,
    cmd_line = cmd_line,
    intern = intern,
    echo = echo
  )

}

conda_run2_windows <-
  function(cmd, args = c(), conda = "auto", envname = NULL,
           cmd_line = paste(shQuote(cmd), paste(args, collapse = " ")),
           intern = FALSE, echo = !intern) {
  conda <- normalizePath(conda_binary(conda))
  local_conda_paths(conda)

  if (identical(envname, "base"))
    envname <- file.path(dirname(conda), "../..")
  else
    envname <- condaenv_resolve(envname)

  if (grepl("[/\\]", envname))
    envname <- normalizePath(envname)

  activate.bat <- normalizePath(file.path(dirname(conda), "activate.bat"),
                                mustWork = FALSE)

  activate_cmd <-
    if (file.exists(activate.bat)) {
      paste("CALL", shQuote(activate.bat), shQuote(envname))
    } else {
      paste("CALL", shQuote(conda), "activate", shQuote(envname))
    }

  fi <- tempfile(fileext = ".bat")
  on.exit(unlink(fi))
  writeLines(c(
    if (!echo) "@echo off",
    activate_cmd,
    cmd_line
  ), fi)

  shell(fi, intern = intern)
}

conda_run2_nix <-
  function(cmd, args = c(), conda = "auto", envname = NULL,
           cmd_line = paste(shQuote(cmd), paste(args, collapse = " ")),
           intern = FALSE, echo = !intern) {
  conda <- normalizePath(conda_binary(conda))
  local_conda_paths(conda)

  if (!identical(envname, "base")) {
    envname <- condaenv_resolve(envname)
    if (grepl("[/\\]", envname))
      envname <- normalizePath(envname)
  }

  fi <- tempfile(fileext = ".sh")
  on.exit(unlink(fi))

  stdout <- if (identical(intern, FALSE)) "" else intern

  if (startsWith(basename(conda), "micromamba")) {

    envflag <- if(grepl("[/\\]", envname)) "-p" else "-n"
    if(echo)
      system2 <- system2t
    result <- system2(conda, c('run', envflag, envname, cmd_line),
                      stdout = stdout)

    error_status <- attr(result, "status")
    if (!is.null(error_status))
      stop("Error ", error_status, " occurred while running conda command")

    return(result)

  }


  activate <- normalizePath(file.path(dirname(conda), "activate"))
  commands <- c(
    paste(".", activate),
    if (!identical(envname, "base"))
      paste("conda activate", shQuote(envname)),
    cmd_line
  )

  # set -x is too verbose, includes all the commands made by conda scripts
  # so we manually echo the top-level commands only
  if (echo)
    commands <- as.vector(rbind(
      paste("echo", shQuote(paste("+", commands))),
      commands))

  writeLines(commands, fi)
  system2(Sys.which("bash"), fi, stdout = stdout)

}


conda_info <- function(conda = "auto") {
  conda <- normalizePath(conda_binary(conda))
  json <- system2(conda, c("info", "--json"), stdout = TRUE)
  jsonlite::parse_json(json, simplifyVector = TRUE)
}

is_conda_python <- function(python) {
  root <- if (is_windows())
    dirname(python)
  else
    dirname(dirname(python))

  file.exists(file.path(root, "conda-meta"))
}


get_python_conda_info <- function(python) {

  stopifnot(is_conda_python(python))

  root <- if (is_windows())
    dirname(python)
  else
    dirname(dirname(python))

  if (dir.exists(file.path(root, "condabin"))) {
    # base conda env
    conda <- if (is_windows())
      file.path(root, "condabin/conda.bat")
    else
      file.path(root, "bin/conda")
  } else {
    # not base env, parse conda-meta history to find the conda binary
    # that created it
    conda <- python_info_condaenv_find(root)
  }

  conda <- normalizePath(conda, winslash = "/", mustWork = FALSE)
  if(!file.exists(conda))
    conda <- NA

  list(
    conda = conda,
    root = normalizePath(root, winslash = "/", mustWork = TRUE)
  )

}


conda_prefix <- function(conda = "auto") {
  conda <- normalizePath(conda_binary(conda),
                         winslash = "/", mustWork = TRUE)
  if(!is_windows())
    # PREFIX/bin/conda
    return(dirname(dirname(conda)))

  # on Windows, conda can be a few places:
  ## PREFIX/Scripts/conda.exe
  ## PREFIX/condabin/conda.bat
  ## PREFIX/conda.exe
  ## others? maybe under mingw-32/ or Library/ or Tools/?
  ## we punt and just ask conda
  system2(conda, c("info", "--base"), stdout = TRUE)
}

conda_bin_paths <- function(conda = "auto") {
  prefix <- conda_prefix(conda)
  paths <- file.path(prefix, c(
    "condabin",
    "bin",
    "Scripts",
    "Library/bin",
    "Library/usr/bin",
    "Library/mingw-w64/bin"
  ))
  paths[dir.exists(paths)]
}


local_conda_paths <- function(conda, action = "prefix",
                              .local_envir = parent.frame()) {
  withr::local_path(conda_bin_paths(conda), action, .local_envir)
}

Try the reticulate package in your browser

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

reticulate documentation built on Sept. 11, 2024, 8:31 p.m.