diff --git a/DESCRIPTION b/DESCRIPTION index 05f433a9..f4609330 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -12,6 +12,7 @@ Depends: Imports: httr, jsonlite, + sys, utils Suggests: knitr, diff --git a/NAMESPACE b/NAMESPACE index 6460abb6..c8a4e8e8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,6 +3,7 @@ export(available_r) export(find_rev) export(get_current) +export(nix_build) export(rix) importFrom(httr,GET) importFrom(httr,content) diff --git a/R/find_rev.R b/R/find_rev.R index bacb5a5d..ed27e2c7 100644 --- a/R/find_rev.R +++ b/R/find_rev.R @@ -388,3 +388,89 @@ USE_RSTUDIO}; } + +#' 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 current working directory in the R session. +#' 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 as "invisible". +#' @export +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( + "Argument `nix_file` must be character of length 1." = + is.character(nix_file) && length(nix_file) == 1L, + "`nix_file` does not exist. Please use a valid path." = + file.exists(nix_file), + "`nix-build` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = + isFALSE(has_nix_build) + ) + exec_mode <- match.arg(exec_mode) + + cat(paste0("Launching `nix-build`", " in ", exec_mode, " mode\n")) + + proc <- switch(exec_mode, + "blocking" = sys::exec_internal("nix-build", nix_file), + "non-blocking" = sys::exec_background("nix-build", nix_file), + stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') + ) + + if (exec_mode == "non-blocking") { + 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("\n==> `nix-build` succeeded!") + } + } + + if (exec_mode == "blocking") { + status <- proc$status + if (status == 0L) { + cat(paste0("\n==> ", sys::as_text(proc$stdout))) + cat("\n==> `nix-build` succeeded!") + } else { + msg <- nix_build_exit_msg() + cat(paste0("`nix-build` failed with ", msg)) + } + } + + # todo (?): clean zombies for background/non-blocking mode + # rm(pid) + + return(invisible(proc)) +} + +#' @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/0-dev_history.Rmd b/dev/0-dev_history.Rmd index 96446ea0..502592ee 100755 --- a/dev/0-dev_history.Rmd +++ b/dev/0-dev_history.Rmd @@ -19,7 +19,7 @@ fusen::fill_description( Title = "Rix: Reproducible Environments with Nix", Description = "Provides helper functions to create reproducible development environments using the Nix package manager.", Version = "0.0.9", - `Authors@R` = c( + `Authors@R` = c person("Bruno", "Rodrigues", email = "bruno@brodrigues.co", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-3211-3689")), person("Philipp", "Baumann", email = "baumann-philipp@protonmail.com", role = "ctb", comment = c(ORCID = "0000-0002-3194-8975")) ) diff --git a/dev/build_envs.Rmd b/dev/build_envs.Rmd index f9fe3a4d..1144e25e 100644 --- a/dev/build_envs.Rmd +++ b/dev/build_envs.Rmd @@ -470,3 +470,94 @@ USE_RSTUDIO}; } ``` + +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 current working directory in the R session. +#' 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 as "invisible". +#' @export +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( + "Argument `nix_file` must be character of length 1." = + is.character(nix_file) && length(nix_file) == 1L, + "`nix_file` does not exist. Please use a valid path." = + file.exists(nix_file), + "`nix-build` not available. To install, we suggest you follow https://zero-to-nix.com/start/install ." = + isFALSE(has_nix_build) + ) + exec_mode <- match.arg(exec_mode) + + cat(paste0("Launching `nix-build`", " in ", exec_mode, " mode\n")) + + proc <- switch(exec_mode, + "blocking" = sys::exec_internal("nix-build", nix_file), + "non-blocking" = sys::exec_background("nix-build", nix_file), + stop('invalid `exec_mode`. Either use "blocking" or "non-blocking"') + ) + + if (exec_mode == "non-blocking") { + 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("\n==> `nix-build` succeeded!") + } + } + + if (exec_mode == "blocking") { + status <- proc$status + if (status == 0L) { + cat(paste0("\n==> ", sys::as_text(proc$stdout))) + cat("\n==> `nix-build` succeeded!") + } else { + msg <- nix_build_exit_msg() + cat(paste0("`nix-build` failed with ", msg)) + } + } + + # todo (?): clean zombies for background/non-blocking mode + # rm(pid) + + return(invisible(proc)) +} + +#' @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/man/nix_build.Rd b/man/nix_build.Rd new file mode 100644 index 00000000..a39a4ecd --- /dev/null +++ b/man/nix_build.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/find_rev.R +\name{nix_build} +\alias{nix_build} +\title{Invoke shell command \code{nix-build} from an R session} +\usage{ +nix_build(project_path = ".", exec_mode = c("blocking", "non-blocking")) +} +\arguments{ +\item{project_path}{Path to the folder where the \code{default.nix} file resides. +session.} + +\item{exec_mode}{Either \code{"blocking"} (default) or \verb{"non-blocking}. This +will either block the R session while the \code{nix-build} shell command is +executed, or run \code{nix-build} in the background ("non-blocking").} +} +\value{ +integer of the process ID (PID) of \code{nix-build} shell command +launched, if \code{nix_build()} call is assigned to an R object. Otherwise, it +will be returned as "invisible". +} +\description{ +Invoke shell command \code{nix-build} from an R session +}