diff --git a/DESCRIPTION b/DESCRIPTION index abd244f..229f237 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -23,7 +23,6 @@ OS_type: unix SystemRequirements: macOS Imports: askpass, - stats, sys, tools, utils, @@ -32,25 +31,6 @@ Imports: URL: https://mac.thecoatlessprofessor.com/macrtools/ Suggests: rstudioapi, + mockery, testthat (>= 3.0.0) -Collate: - 'cli-custom.R' - 'assertions.R' - 'blocks.R' - 'renviron.R' - 'shell.R' - 'utils.R' - 'installers.R' - 'gfortran.R' - 'macos-versions.R' - 'paths.R' - 'recipes.R' - 'system-checks.R' - 'xcode-cli.R' - 'toolchain.R' - 'version-check.R' - 'xcode-app-ide.R' - 'xcode-select.R' - 'xcodebuild.R' - 'zzz.R' Config/testthat/edition: 3 diff --git a/R/assertions.R b/R/assertions.R index fb9e0b1..bd0e5f6 100644 --- a/R/assertions.R +++ b/R/assertions.R @@ -1,6 +1,3 @@ -#' @include cli-custom.R - - #' @title Assert a condition #' #' @description @@ -14,8 +11,10 @@ #' @rdname assert #' @export assert <- function(condition, message = NULL, call = caller_env()) { - if (isFALSE(condition)) { - cli_error(message, call = call) + if (base::isFALSE(condition)) { + cli::cli_abort(c( + "{.pkg macrtools}: {message}" + ), call = call) } } @@ -23,9 +22,10 @@ assert <- function(condition, message = NULL, call = caller_env()) { #' @export assert_mac <- function(call = caller_env()) { if (!is_macos()) { - cli_error(c( - "This function requires macOS.", - "The current operating system is {.val {tolower(Sys.info()[['sysname']])}}." + current_os <- base::tolower(base::Sys.info()[['sysname']]) + cli::cli_abort(c( + "{.pkg macrtools}: This function requires macOS.", + "{.pkg macrtools}: The current operating system is {.val {current_os}}." ), call = call, advice = "macrtools only works on macOS systems with Intel or Apple Silicon processors.") @@ -39,9 +39,9 @@ assert_macos_supported <- function(call = caller_env()) { if (!is_macos_r_supported()) { mac_version <- shell_mac_version() - cli_error(c( - "Your macOS version {.val {mac_version}} is not supported.", - "Supported versions: macOS High Sierra (10.13) through macOS Sequoia (15.x)." + cli::cli_abort(c( + "{.pkg macrtools}: Your macOS version {.val {mac_version}} is not supported.", + "{.pkg macrtools}: Supported versions: macOS High Sierra (10.13) through macOS Sequoia (15.x)." ), call = call, advice = "Please upgrade your macOS to a supported version or use an alternative method to install development tools.") @@ -52,9 +52,10 @@ assert_macos_supported <- function(call = caller_env()) { #' @export assert_aarch64 <- function(call = caller_env()) { if (!is_aarch64()) { - cli_error(c( - "This function requires an Apple Silicon (M-series) Mac.", - "Current architecture: {.val {system_arch()}}." + arch <- system_arch() + cli::cli_abort(c( + "{.pkg macrtools}: This function requires an Apple Silicon (M-series) Mac.", + "{.pkg macrtools}: Current architecture: {.val {arch}}." ), call = call, advice = "This feature is specifically designed for Apple Silicon processors (M1, M2, M3, etc.). Intel Macs require different components.") @@ -65,9 +66,10 @@ assert_aarch64 <- function(call = caller_env()) { #' @export assert_x86_64 <- function(call = caller_env()) { if (!is_x86_64()) { - cli_error(c( - "This function requires an Intel-based Mac.", - "Current architecture: {.val {system_arch()}}." + arch <- system_arch() + cli::cli_abort(c( + "{.pkg macrtools}: This function requires an Intel-based Mac.", + "{.pkg macrtools}: Current architecture: {.val {arch}}." ), call = call, advice = "This feature is specifically designed for Intel processors. Apple Silicon Macs require different components.") @@ -79,10 +81,10 @@ assert_x86_64 <- function(call = caller_env()) { assert_r_version_supported <- function(call = caller_env()) { if (!(is_r_version("4.0") || is_r_version("4.1") || is_r_version("4.2") || is_r_version("4.3") || is_r_version("4.4"))) { - version_number <- paste(R.version$major, R.version$minor, sep = ".") - cli_error(c( - "The installed R version {.val {version_number}} is not supported.", - "Supported versions: R 4.0.x through R 4.4.x." + version_number <- base::paste(base::R.version$major, base::R.version$minor, sep = ".") + cli::cli_abort(c( + "{.pkg macrtools}: The installed R version {.val {version_number}} is not supported.", + "{.pkg macrtools}: Supported versions: R 4.0.x through R 4.4.x." ), call = call, advice = "Please upgrade or downgrade your R installation to a supported version.") diff --git a/R/blocks.R b/R/blocks.R index 299e84c..78dde43 100644 --- a/R/blocks.R +++ b/R/blocks.R @@ -7,7 +7,7 @@ # Light modifications have occurred to remove use of `ui_*()` platform_line_ending = function() { - if (.Platform$OS.type == "windows") "\r\n" else "\n" + if (base::.Platform$OS.type == "windows") "\r\n" else "\n" } read_utf8 = function(path, n = -1L) { @@ -15,51 +15,51 @@ read_utf8 = function(path, n = -1L) { } write_utf8 = function(path, lines, append = FALSE, line_ending = NULL) { - stopifnot(is.character(path)) - stopifnot(is.character(lines)) + base::stopifnot(base::is.character(path)) + base::stopifnot(base::is.character(lines)) file_mode = if (append) "ab" else "wb" - con = file(path, open = file_mode, encoding = "utf-8") + con = base::file(path, open = file_mode, encoding = "utf-8") - if (is.null(line_ending)) { + if (base::is.null(line_ending)) { line_ending = platform_line_ending() } # convert embedded newlines - lines = gsub("\r?\n", line_ending, lines) + lines = base::gsub("\r?\n", line_ending, lines) base::writeLines(base::enc2utf8(lines), con, sep = line_ending, useBytes = TRUE) - close(con) + base::close(con) - invisible(TRUE) + base::invisible(TRUE) } seq2 = function (from, to) { - if (length(from) != 1) { - stop(sprintf("%s must be length one.", from)) + if (base::length(from) != 1) { + base::stop(base::sprintf("%s must be length one.", from)) } - if (length(to) != 1) { - stop(sprintf("%s must be length one.", to)) + if (base::length(to) != 1) { + base::stop(base::sprintf("%s must be length one.", to)) } if (from > to) { - integer(0) + base::integer(0) } else { - seq.int(from, to) + base::seq.int(from, to) } } block_append = function(desc, value, path, - block_start = "# <<<", - block_end = "# >>>", - block_prefix = NULL, - block_suffix = NULL, - sort = FALSE) { + block_start = "# <<<", + block_end = "# >>>", + block_prefix = NULL, + block_suffix = NULL, + sort = FALSE) { - if (!is.null(path) && file.exists(path)) { + if (!base::is.null(path) && base::file.exists(path)) { lines = read_utf8(path) - if (all(value %in% lines)) { + if (base::all(value %in% lines)) { return(FALSE) } @@ -68,9 +68,9 @@ block_append = function(desc, value, path, block_lines = NULL } - message("Adding ", desc, " to ", path) + base::message("Adding ", desc, " to ", path) - if (is.null(block_lines)) { + if (base::is.null(block_lines)) { # changed as we have a cold start and want to enforce a block being present write_utf8(path, block_create(value, block_start, block_end), append = TRUE) return(TRUE) @@ -81,15 +81,15 @@ block_append = function(desc, value, path, end = block_lines[[2]] block = lines[seq2(start, end)] - new_lines = union(block, value) + new_lines = base::union(block, value) if (sort) { - new_lines = sort(new_lines) + new_lines = base::sort(new_lines) } - lines = c( + lines = base::c( lines[seq2(1, start - 1L)], new_lines, - lines[seq2(end + 1L, length(lines))] + lines[seq2(end + 1L, base::length(lines))] ) write_utf8(path, lines) @@ -97,35 +97,35 @@ block_append = function(desc, value, path, } block_replace = function(desc, value, path, - block_start = "# <<<", - block_end = "# >>>") { - if (!is.null(path) && file.exists(path)) { + block_start = "# <<<", + block_end = "# >>>") { + if (!base::is.null(path) && base::file.exists(path)) { lines = read_utf8(path) block_lines = block_find(lines, block_start, block_end) } else { block_lines = NULL } - if (is.null(block_lines)) { - message("Copy and paste the following lines into ", path, ":") - paste0(c(block_start, value, block_end), collapse = "\n") - return(invisible(FALSE)) + if (base::is.null(block_lines)) { + base::message("Copy and paste the following lines into ", path, ":") + base::paste0(base::c(block_start, value, block_end), collapse = "\n") + return(base::invisible(FALSE)) } start = block_lines[[1]] end = block_lines[[2]] block = lines[seq2(start, end)] - if (identical(value, block)) { - return(invisible(FALSE)) + if (base::identical(value, block)) { + return(base::invisible(FALSE)) } - message("Replacing ", desc, " in ", path) + base::message("Replacing ", desc, " in ", path) - lines = c( + lines = base::c( lines[seq2(1, start - 1L)], value, - lines[seq2(end + 1L, length(lines))] + lines[seq2(end + 1L, base::length(lines))] ) write_utf8(path, lines) } @@ -139,25 +139,25 @@ block_show = function(path, block_start = "# <<<", block_end = "# >>>") { block_find = function(lines, block_start = "# <<<", block_end = "# >>>") { # No file - if (is.null(lines)) { + if (base::is.null(lines)) { return(NULL) } - start = which(lines == block_start) - end = which(lines == block_end) + start = base::which(lines == block_start) + end = base::which(lines == block_end) # No block - if (length(start) == 0 && length(end) == 0) { + if (base::length(start) == 0 && base::length(end) == 0) { return(NULL) } - if (!(length(start) == 1 && length(end) == 1 && start < end)) { - stop("Invalid block specification.") + if (!(base::length(start) == 1 && base::length(end) == 1 && start < end)) { + base::stop("Invalid block specification.") } - c(start + 1L, end - 1L) + base::c(start + 1L, end - 1L) } block_create = function(lines = character(), block_start = "# <<<", block_end = "# >>>") { - c("\n", block_start, unique(lines), block_end) + base::c("\n", block_start, base::unique(lines), block_end) } diff --git a/R/cli-custom.R b/R/cli-custom.R deleted file mode 100644 index 5780580..0000000 --- a/R/cli-custom.R +++ /dev/null @@ -1,155 +0,0 @@ -#' CLI Utility Functions -#' -#' Utility functions for consistent CLI messaging throughout the package. -#' -#' @name cli-utils -#' @keywords internal -NULL - -#' Format a message with a package prefix -#' -#' @param msg The message to format -#' @param .envir Environment to evaluate in -#' @return A formatted string with package prefix -#' @keywords internal -format_msg <- function(msg, .envir = parent.frame()) { - paste0("{.pkg macrtools}: ", cli::format_inline(msg, .envir = .envir)) -} - -#' Format a path for display -#' -#' @param path The file path to format -#' @return A formatted string with path styling -#' @keywords internal -format_path <- function(path) { - paste0("{.file ", path, "}") -} - -#' Format a command for display -#' -#' @param cmd The command to format -#' @return A formatted string with code styling -#' @keywords internal -format_cmd <- function(cmd) { - paste0("{.code ", cmd, "}") -} - -#' Display an info message -#' -#' @param ... Message parts, passed to cli::format_inline -#' @param .envir Environment to evaluate in -#' @keywords internal -cli_info <- function(..., .envir = parent.frame()) { - cli::cli_alert_info(format_msg(paste0(...), .envir = .envir)) -} - -#' Display a success message -#' -#' @param ... Message parts, passed to cli::format_inline -#' @param .envir Environment to evaluate in -#' @keywords internal -cli_success <- function(..., .envir = parent.frame()) { - cli::cli_alert_success(format_msg(paste0(...), .envir = .envir)) -} - -#' Display a warning message -#' -#' @param ... Message parts, passed to cli::format_inline -#' @param .envir Environment to evaluate in -#' @keywords internal -cli_warning <- function(..., .envir = parent.frame()) { - cli::cli_alert_warning(format_msg(paste0(...), .envir = .envir)) -} - -#' Display a danger/important message -#' -#' @param ... Message parts, passed to cli::format_inline -#' @param .envir Environment to evaluate in -#' @keywords internal -cli_danger <- function(..., .envir = parent.frame()) { - cli::cli_alert_danger(format_msg(paste0(...), .envir = .envir)) -} - -#' Throw an error with a formatted message -#' -#' @param ... Message parts, passed to cli::format_inline -#' @param .envir Environment to evaluate in -#' @param call The calling environment -#' @param advice Optional advice to provide to the user -#' @keywords internal -cli_error <- function(..., .envir = parent.frame(), call = caller_env(), advice = NULL) { - msg <- format_msg(paste0(...), .envir = .envir) - if (!is.null(advice)) { - msg <- c(msg, "i" = advice) - } - cli::cli_abort(msg, call = call) -} - -#' Display a waiting message -#' -#' @param ... Message parts, passed to cli::format_inline -#' @param .envir Environment to evaluate in -#' @keywords internal -cli_waiting <- function(..., .envir = parent.frame()) { - cli::cli_alert_info(paste0("{.pkg macrtools}: {.file ", paste0(...), "} {.spinner}")) -} - -#' Display the start of a process -#' -#' @param msg The process description -#' @param .envir Environment to evaluate in -#' @param total The total number of steps (default: 100) -#' @param clear Whether to clear the progress bar when done (default: FALSE) -#' @return The ID of the progress bar -#' @keywords internal -cli_process_start <- function(msg, .envir = parent.frame(), total = 100, clear = FALSE) { - cli::cli_progress_bar( - format = paste0( - "{.pkg macrtools}: {.strong {.msg {msg}}} {.progress_bar} {.percent {.percent}} {.spinner}" - ), - format_done = paste0( - "{.pkg macrtools}: {.strong {.msg {msg}}} {cli::symbol$tick} Done!" - ), - msg = msg, - total = total, - clear = clear, - .envir = .envir - ) -} - -#' Update a process progress -#' -#' @param id The progress bar ID -#' @param ratio The progress ratio (0-1) or number of steps completed -#' @param status_msg Optional status message to display -#' @keywords internal -cli_process_update <- function(id, ratio, status_msg = NULL) { - if (!is.null(status_msg)) { - cli::cli_progress_update(id = id, set = ratio, msg = status_msg) - } else { - cli::cli_progress_update(id = id, set = ratio) - } -} - -#' Complete a process -#' -#' @param id The progress bar ID -#' @param msg Optional completion message -#' @keywords internal -cli_process_done <- function(id, msg = NULL) { - if (!is.null(msg)) { - cli::cli_progress_update(id = id, msg = msg) - } - cli::cli_progress_done(id = id) -} - -#' Display a detailed system update -#' -#' @param title Title of the update section -#' @param items Vector of items to display as a bulleted list -#' @keywords internal -cli_system_update <- function(title, items) { - cli::cli_h3(title) - cli::cli_ul(items) - cli::cli_end() -} diff --git a/R/gfortran.R b/R/gfortran.R index a8f3502..ab8d6f8 100644 --- a/R/gfortran.R +++ b/R/gfortran.R @@ -1,7 +1,3 @@ -#' @include shell.R utils.R installers.R renviron.R cli-custom.R -NULL - - #' Find, Install, or Uninstall gfortran #' #' Set of functions that seek to identify whether gfortran was installed, @@ -39,10 +35,10 @@ is_gfortran_installed <- function() { # Figure out installation directory install_dir <- gfortran_install_location() - path_gfortran <- file.path(install_dir, "gfortran") + path_gfortran <- base::file.path(install_dir, "gfortran") # Check if the directory is present - dir.exists(path_gfortran) + base::dir.exists(path_gfortran) } #' @export @@ -156,36 +152,50 @@ gfortran_version <- function() { #' `NULL`. #' @export #' @rdname gfortran -gfortran_install <- function(password = getOption("macrtools.password"), verbose = TRUE) { +gfortran_install <- function(password = base::getOption("macrtools.password"), verbose = TRUE) { assert_mac() assert_macos_supported() assert_r_version_supported() - if(isTRUE(is_gfortran_installed())) { + if(base::isTRUE(is_gfortran_installed())) { if(verbose) { - cli_info(c( - "gfortran is already installed at {.path {file.path(gfortran_install_location(), 'gfortran')}}.", - "Version information: {.val {tryCatch(sys::as_text(sys::exec_internal('gfortran', '--version')$stdout), error = function(e) 'Unknown')}}" + # Get gfortran path and version info + install_path <- base::file.path(gfortran_install_location(), 'gfortran') + version_info <- base::tryCatch( + sys::as_text(sys::exec_internal('gfortran', '--version')$stdout), + error = function(e) 'Unknown' + ) + + cli::cli_alert_info("{.pkg macrtools}: gfortran is already installed.") + cli::cli_bullets(c( + "Installation path: {.path {install_path}}", + "Version information: {.val {version_info}}" )) + cli::cli_text("") # Add spacing } - return(invisible(TRUE)) + return(base::invisible(TRUE)) } if (verbose) { - cli_info(c( - "Preparing to download and install gfortran...", - "Architecture: {.val {system_arch()}}", - "R version: {.val {paste(R.version$major, R.version$minor, sep='.')}}", - "Expected installation time: 2-5 minutes on a broadband connection." + # Get system info + arch_info <- system_arch() + r_version <- base::paste(base::R.version$major, base::R.version$minor, sep='.') + + cli::cli_alert_info("{.pkg macrtools}: Preparing to download and install gfortran.") + cli::cli_bullets(c( + "Architecture: {.val {arch_info}}", + "R version: {.val {r_version}}", + "Expected installation time: 2-5 minutes on a broadband connection" )) + cli::cli_text("") # Add spacing } # Figure out installation directory install_dir <- gfortran_install_location() # Establish a path - path_gfortran_bin <- file.path(install_dir, "gfortran", "bin") - path_variable <- paste0("${PATH}:", path_gfortran_bin) + path_gfortran_bin <- base::file.path(install_dir, "gfortran", "bin") + path_variable <- base::paste0("${PATH}:", path_gfortran_bin) # Prompt for password if not found entered_password_gfortran <- force_password(password) @@ -207,67 +217,78 @@ gfortran_install <- function(password = getOption("macrtools.password"), verbose } else if (is_aarch64()) { if (is_r_version("4.2")) { if (verbose) { - cli_info(c( - "Installing gfortran 12 for Apple Silicon with R 4.2", + install_path <- install_location() + cli::cli_alert_info("{.pkg macrtools}: Installing gfortran 12 for Apple Silicon with R 4.2") + cli::cli_bullets(c( "Source: {.url https://mac.r-project.org/tools/}", - "Target installation: {.path {install_location()}}" + "Target installation: {.path {install_path}}" )) + cli::cli_text("") # Add spacing } gfortran_status <- install_gfortran_12_arm( password = entered_password_gfortran, verbose = verbose) } else if(is_r_version("4.1")) { if (verbose) { - cli_info(c( - "Installing gfortran 11 for Apple Silicon with R 4.1", + install_path <- install_location() + cli::cli_alert_info("{.pkg macrtools}: Installing gfortran 11 for Apple Silicon with R 4.1") + cli::cli_bullets(c( "Source: {.url https://mac.r-project.org/libs-arm64/}", - "Target installation: {.path {install_location()}}" + "Target installation: {.path {install_path}}" )) + cli::cli_text("") # Add spacing } gfortran_status <- install_gfortran_11_arm( password = entered_password_gfortran, verbose = verbose) } else { - cli_error(c( - "Unable to install gfortran for Apple Silicon (arm64/aarch64).", - "Official R support for Apple Silicon began in R 4.1.", - "Please upgrade R to version 4.1 or higher." - ), - advice = "Visit https://cran.r-project.org/bin/macosx/ to download a compatible R version for Apple Silicon.") - return(invisible(FALSE)) + cli::cli_abort(c( + "{.pkg macrtools}: Unable to install gfortran for Apple Silicon (arm64/aarch64).", + "i" = "Official R support for Apple Silicon began in R 4.1.", + "i" = "Please upgrade R to version 4.1 or higher.", + "i" = "Visit https://cran.r-project.org/bin/macosx/ to download a compatible R version for Apple Silicon." + )) + return(base::invisible(FALSE)) } } else { - cli_error(c( - "Unsupported macOS architecture: {.val {system_arch()}}", - "Only Intel (x86_64) and Apple Silicon (arm64/aarch64) architectures are supported." + arch <- system_arch() + cli::cli_abort(c( + "{.pkg macrtools}: Unsupported macOS architecture: {.val {arch}}", + "i" = "Only Intel (x86_64) and Apple Silicon (arm64/aarch64) architectures are supported." )) - return(invisible(FALSE)) + return(base::invisible(FALSE)) } } - if(isFALSE(gfortran_status)) { - cli_error(c( - "Failed to install gfortran.", - "Installation target: {.path {gfortran_install_location()}}", - "Architecture: {.val {system_arch()}}", - "R version: {.val {paste(R.version$major, R.version$minor, sep='.')}}" - ), - advice = "Please try to manually install following the instructions at: https://mac.thecoatlessprofessor.com/macrtools/reference/gfortran.html#installing-gfortran") - return(invisible(FALSE)) + if(base::isFALSE(gfortran_status)) { + install_path <- gfortran_install_location() + arch_info <- system_arch() + r_version <- base::paste(base::R.version$major, base::R.version$minor, sep='.') + + cli::cli_abort(c( + "{.pkg macrtools}: Failed to install gfortran.", + "i" = "Installation target: {.path {install_path}}", + "i" = "Architecture: {.val {arch_info}}", + "i" = "R version: {.val {r_version}}", + "i" = "Please try to manually install following the instructions at: https://mac.thecoatlessprofessor.com/macrtools/reference/gfortran.html#installing-gfortran" + )) + return(base::invisible(FALSE)) } renviron_gfortran_path(path_variable) if (verbose) { - cli_success(c( - "gfortran was successfully installed!", - "Installation location: {.path {file.path(gfortran_install_location(), 'gfortran')}}", + install_path <- base::file.path(gfortran_install_location(), 'gfortran') + cli::cli_alert_success("{.pkg macrtools}: gfortran was successfully installed!") + cli::cli_bullets(c( + "Installation location: {.path {install_path}}", "PATH environment variable has been updated in your ~/.Renviron file.", "You may need to restart R for the changes to take effect." )) + cli::cli_text("") # Add spacing } - return(invisible(TRUE)) + return(base::invisible(TRUE)) } #' @section Uninstalling `gfortran`: @@ -301,46 +322,52 @@ gfortran_install <- function(password = getOption("macrtools.password"), verbose #' #' @export #' @rdname gfortran -gfortran_uninstall <- function(password = getOption("macrtools.password"), verbose = TRUE) { +gfortran_uninstall <- function(password = base::getOption("macrtools.password"), verbose = TRUE) { assert_mac() - if(isFALSE(is_gfortran_installed())) { + if(base::isFALSE(is_gfortran_installed())) { if(verbose) { - cli_info("gfortran is not installed.") + cli::cli_alert_info("{.pkg macrtools}: gfortran is not installed.") + cli::cli_text("") # Add spacing } - return(invisible(TRUE)) + return(base::invisible(TRUE)) } # Figure out installation directories install_dir <- gfortran_install_location() - path_gfortran <- file.path(install_dir, "gfortran") - path_bin_gfortran <- file.path(install_dir, "bin", "gfortran") + path_gfortran <- base::file.path(install_dir, "gfortran") + path_bin_gfortran <- base::file.path(install_dir, "bin", "gfortran") if (verbose) { - cli_info("Uninstalling gfortran...") + cli::cli_alert_info("{.pkg macrtools}: Uninstalling gfortran.") + cli::cli_bullets(c( + "Path to remove: {.path {path_gfortran}}", + "Binary to remove: {.path {path_bin_gfortran}}" + )) + cli::cli_text("") # Add spacing } gfortran_uninstall_status <- shell_execute( - paste0("rm -rf ", path_gfortran, " ", path_bin_gfortran), + base::paste0("rm -rf ", path_gfortran, " ", path_bin_gfortran), sudo = TRUE, password = password) - gfortran_uninstall_clean <- identical(gfortran_uninstall_status, 0L) - if(isFALSE(gfortran_uninstall_clean)) { - cli_error(c( - "We were not able to uninstall gfortran.", - "Please try to manually uninstall using:", - "{.url https://mac.thecoatlessprofessor.com/macrtools/reference/gfortran.html#uninstalling-gfortran}" + gfortran_uninstall_clean <- base::identical(gfortran_uninstall_status, 0L) + if(base::isFALSE(gfortran_uninstall_clean)) { + cli::cli_abort(c( + "{.pkg macrtools}: We were not able to uninstall gfortran.", + "i" = "Please try to manually uninstall using: https://mac.thecoatlessprofessor.com/macrtools/reference/gfortran.html#uninstalling-gfortran" )) - return(invisible(FALSE)) + return(base::invisible(FALSE)) } if (verbose) { - cli_success("gfortran was successfully uninstalled!") + cli::cli_alert_success("{.pkg macrtools}: gfortran was successfully uninstalled!") + cli::cli_text("") # Add spacing } - return(invisible(gfortran_uninstall_clean)) + return(base::invisible(gfortran_uninstall_clean)) } #' @section Updating `gfortran`: @@ -359,7 +386,7 @@ gfortran_uninstall <- function(password = getOption("macrtools.password"), verbo #' #' @export #' @rdname gfortran -gfortran_update <- function(password = getOption("macrtools.password"), verbose = TRUE) { +gfortran_update <- function(password = base::getOption("macrtools.password"), verbose = TRUE) { assert_mac() assert_aarch64() assert(is_gfortran_installed(), "gfortran must be installed") @@ -369,16 +396,22 @@ gfortran_update <- function(password = getOption("macrtools.password"), verbose # Figure out installation directory install_dir <- install_location() - path_gfortran_update <- file.path(install_dir, "gfortran", "bin", "gfortran-update-sdk") + path_gfortran_update <- base::file.path(install_dir, "gfortran", "bin", "gfortran-update-sdk") # Verify that the tool exists - if (!file.exists(path_gfortran_update)) { - cli_error("Could not find gfortran-update-sdk at {.path {path_gfortran_update}}") - return(invisible(FALSE)) + if (!base::file.exists(path_gfortran_update)) { + cli::cli_abort("{.pkg macrtools}: Could not find gfortran-update-sdk at {.path {path_gfortran_update}}") + return(base::invisible(FALSE)) } if (verbose) { - cli_info("Updating gfortran...") + cli::cli_alert_info("{.pkg macrtools}: Updating gfortran.") + cli::cli_bullets(c( + "Update tool: {.path {path_gfortran_update}}", + "Architecture: {.val {system_arch()}}", + "R version: {.val {base::paste(base::R.version$major, base::R.version$minor, sep='.')}}" + )) + cli::cli_text("") # Add spacing } gfortran_update_status <- shell_execute( @@ -386,37 +419,36 @@ gfortran_update <- function(password = getOption("macrtools.password"), verbose sudo = TRUE, password = password) - gfortran_update_clean <- identical(gfortran_update_status, 0L) - if(isFALSE(gfortran_update_clean)) { - cli_error(c( - "We were not able to update gfortran.", - "Please try to manually update using:", - "{.url https://mac.thecoatlessprofessor.com/macrtools/reference/gfortran.html#updating-gfortran}" + gfortran_update_clean <- base::identical(gfortran_update_status, 0L) + if(base::isFALSE(gfortran_update_clean)) { + cli::cli_abort(c( + "{.pkg macrtools}: We were not able to update gfortran.", + "i" = "Please try to manually update using: https://mac.thecoatlessprofessor.com/macrtools/reference/gfortran.html#updating-gfortran" )) - return(invisible(FALSE)) + return(base::invisible(FALSE)) } if (verbose) { - cli_success("gfortran was successfully updated!") + cli::cli_alert_success("{.pkg macrtools}: gfortran was successfully updated!") + cli::cli_text("") # Add spacing } - return(invisible(gfortran_update_clean)) + return(base::invisible(gfortran_update_clean)) } - gfortran <- function(args) { - out <- tryCatch( + out <- base::tryCatch( sys::exec_internal("gfortran", args = args, error = FALSE), error = function(e) { - list( + base::list( output = e, status = -127L ) } ) - structure( - list( + base::structure( + base::list( output = sys::as_text(out$stdout), status = out$status ), @@ -427,19 +459,19 @@ gfortran <- function(args) { #' Download and Install gfortran 8.2 for Intel Macs #' #' @noRd -install_gfortran_82_mojave <- function(password = getOption("macrtools.password"), +install_gfortran_82_mojave <- function(password = base::getOption("macrtools.password"), verbose = TRUE) { # Key the necessary download steps gfortran_82_url <- "https://mac.r-project.org/tools/gfortran-8.2-Mojave.dmg" - gfortran_dmg_name <- basename(gfortran_82_url) + gfortran_dmg_name <- base::basename(gfortran_82_url) # Download the dmg file gfortran_path <- binary_download(gfortran_82_url, verbose = verbose) # Establish where in the dmg the installer package is path_to_pkg <- - file.path( + base::file.path( tools::file_path_sans_ext(gfortran_dmg_name), tools::file_path_sans_ext(gfortran_dmg_name), "gfortran.pkg" @@ -454,18 +486,18 @@ install_gfortran_82_mojave <- function(password = getOption("macrtools.password" ) if (!success) { - cli_error("Error installing the gfortran package") + cli::cli_abort("{.pkg macrtools}: Error installing the gfortran package") return(FALSE) } # Remove unused gfortran file - unlink(gfortran_path) + base::unlink(gfortran_path) - return(invisible(TRUE)) + return(base::invisible(TRUE)) } -install_gfortran_12_arm <- function(password = getOption("macrtools.password"), +install_gfortran_12_arm <- function(password = base::getOption("macrtools.password"), verbose = TRUE) { gfortran_12_url <- "https://mac.r-project.org/tools/gfortran-12.0.1-20220312-is-darwin20-arm64.tar.xz" @@ -487,7 +519,7 @@ install_gfortran_12_arm <- function(password = getOption("macrtools.password"), } -install_gfortran_11_arm <- function(password = getOption("macrtools.password"), +install_gfortran_11_arm <- function(password = base::getOption("macrtools.password"), verbose = TRUE) { gfortran_11_url <- "https://mac.r-project.org/libs-arm64/gfortran-f51f1da0-darwin20.0-arm64.tar.gz" @@ -517,7 +549,7 @@ install_gfortran_11_arm <- function(password = getOption("macrtools.password"), #' #' @noRd install_gfortran_12_2_universal <- function( - password = getOption("macrtools.password"), + password = base::getOption("macrtools.password"), verbose = TRUE) { # URL diff --git a/R/installers.R b/R/installers.R index a81e4d9..08575b0 100644 --- a/R/installers.R +++ b/R/installers.R @@ -1,57 +1,54 @@ -#' @include utils.R shell.R cli-custom.R -NULL - # Installation directories ---- install_strip_level <- function(arch = system_arch()) { - switch( + base::switch( arch, "arm64" = 3, "aarch64" = 3, "x86_64" = 2, - cli_error("Architecture {.val {arch}} not recognized. Please ensure you are on either an Apple Silicon (arm64) or Intel (x86_64) system.") + cli::cli_abort("{.pkg macrtools}: Architecture {.val {arch}} not recognized. Please ensure you are on either an Apple Silicon (arm64) or Intel (x86_64) system.") ) } recipe_binary_install_strip_level <- function(arch = system_arch()) { if (is_r_version("4.3") || is_r_version("4.4")) { - switch( + base::switch( arch, "arm64" = 3, "aarch64" = 3, "x86_64" = 3, - cli_error("Architecture {.val {arch}} not recognized. Please ensure you are on either an Apple Silicon (arm64) or Intel (x86_64) system.") + cli::cli_abort("{.pkg macrtools}: Architecture {.val {arch}} not recognized. Please ensure you are on either an Apple Silicon (arm64) or Intel (x86_64) system.") ) } else if (is_r_version("4.0") || is_r_version("4.1") || is_r_version("4.2")) { install_strip_level() } else { - cli_error("Unsupported R version. We only support recipe binary installation for R 4.0.x through 4.4.x.") + cli::cli_abort("{.pkg macrtools}: Unsupported R version. We only support recipe binary installation for R 4.0.x through 4.4.x.") } } install_location <- function(arch = system_arch()) { - switch( + base::switch( arch, "arm64" = install_directory_arm64(), "aarch64" = install_directory_arm64(), "x86_64" = install_directory_x86_64(), - cli_error("Architecture {.val {arch}} not recognized. Please ensure you are on either an Apple Silicon (arm64) or Intel (x86_64) system.") + cli::cli_abort("{.pkg macrtools}: Architecture {.val {arch}} not recognized. Please ensure you are on either an Apple Silicon (arm64) or Intel (x86_64) system.") ) } recipe_binary_install_location <- function(arch = system_arch()) { if (is_r_version("4.3") || is_r_version("4.4")) { - switch( + base::switch( arch, "arm64" = install_directory_arm64(), "aarch64" = install_directory_arm64(), "x86_64" = install_directory_x86_64_r_version_4_3(), - cli_error("Architecture {.val {arch}} not recognized. Please ensure you are on either an Apple Silicon (arm64) or Intel (x86_64) system.") + cli::cli_abort("{.pkg macrtools}: Architecture {.val {arch}} not recognized. Please ensure you are on either an Apple Silicon (arm64) or Intel (x86_64) system.") ) } else if (is_r_version("4.0") || is_r_version("4.1") || is_r_version("4.2")) { install_location() } else { - cli_error("Unsupported R version. We only support recipe binary installation for R 4.0.x through 4.4.x.") + cli::cli_abort("{.pkg macrtools}: Unsupported R version. We only support recipe binary installation for R 4.0.x through 4.4.x.") } } @@ -61,25 +58,34 @@ gfortran_install_location <- function(arch = system_arch()) { } else if (is_r_version("4.0") || is_r_version("4.1") || is_r_version("4.2")) { install_location() } else { - cli_error("Unsupported R version. We only support gfortran installation for R 4.0.x through 4.4.x.") + cli::cli_abort("{.pkg macrtools}: Unsupported R version. We only support gfortran installation for R 4.0.x through 4.4.x.") } } -create_install_location <- function(arch = system_arch(), password = getOption("macrtools.password")) { +create_install_location <- function(arch = system_arch(), password = base::getOption("macrtools.password")) { install_dir <- install_location(arch) # Verify installation directory exists. If it doesn't, create it. if (base::dir.exists(install_dir)) { - return(invisible(TRUE)) + return(base::invisible(TRUE)) } - cli_info(c( - "Creating installation directory for binaries.", + # Get current directory permissions + dir_perms <- base::tryCatch( + base::paste(base::strsplit(base::system(base::paste('ls -ld', base::dirname(install_dir)), intern = TRUE), ' ')[[1]][1:3], collapse = ' '), + error = function(e) "Unknown" + ) + + current_user <- base::Sys.info()['user'] + + cli::cli_alert_info("{.pkg macrtools}: Creating installation directory for binaries.") + cli::cli_bullets(c( "Architecture: {.val {arch}}", "Target directory: {.path {install_dir}}", "Permission level: Administrative privileges required", - "Current user: {.val {Sys.info()['user']}}" + "Current user: {.val {current_user}}" )) + cli::cli_text("") # Add spacing dir_creation_status <- shell_execute( base::paste("mkdir", "-p", install_dir), @@ -89,26 +95,32 @@ create_install_location <- function(arch = system_arch(), password = getOption(" dir_creation_clean <- base::identical(dir_creation_status, 0L) if (!dir_creation_clean) { - cli_error(c( - "Failed to create installation directory.", + cli::cli_abort(c( + "{.pkg macrtools}: Failed to create installation directory.", "Directory: {.path {install_dir}}", "Status code: {.val {dir_creation_status}}", "Architecture: {.val {arch}}", - "Current permissions: {.val {base::paste(base::strsplit(base::system(paste('ls -ld', base::dirname(install_dir)), intern = TRUE), ' ')[[1]][1:3], collapse = ' ')}}" - ), - advice = "Check that you have sufficient administrative privileges and the parent directory exists.") + "Current permissions: {.val {dir_perms}}", + "i" = "Check that you have sufficient administrative privileges and the parent directory exists." + )) } else { - cli_success(c( - "Installation directory created successfully.", + # Get owner of new directory + new_owner <- base::tryCatch( + base::paste(base::strsplit(base::system(base::paste('ls -ld', install_dir), intern = TRUE), ' ')[[1]][3], collapse = ' '), + error = function(e) "Unknown" + ) + + cli::cli_alert_success("{.pkg macrtools}: Installation directory created successfully.") + cli::cli_bullets(c( "Directory: {.path {install_dir}}", - "Owner: {.val {base::paste(base::strsplit(base::system(paste('ls -ld', install_dir), intern = TRUE), ' ')[[1]][3], collapse = ' ')}}" + "Owner: {.val {new_owner}}" )) + cli::cli_text("") # Add spacing } - return(invisible(dir_creation_clean)) + return(base::invisible(dir_creation_clean)) } - # Tar Installation ---- #' Download Binary Packages @@ -134,13 +146,15 @@ create_install_location <- function(arch = system_arch(), password = getOption(" binary_download <- function(url, binary_file_name = base::basename(url), verbose = TRUE, mode = "wb", timeout = 600) { if (verbose) { - cli_info(c( - "Starting download of binary package.", + temp_dir <- base::tempdir() + + cli::cli_alert_info("{.pkg macrtools}: Starting download of binary package.") + cli::cli_bullets(c( "Source URL: {.url {url}}", "File name: {.file {binary_file_name}}", - "Temporary directory: {.path {base::tempdir()}}", - "Internet connection: {.val {Sys.getenv('R_NETWORK_STATUS', unset = 'Unknown')}}" + "Temporary directory: {.path {temp_dir}}" )) + cli::cli_text("") # Add spacing } # Configure temporary file location @@ -148,12 +162,16 @@ binary_download <- function(url, binary_file_name = base::basename(url), verbose # Create progress bar if (verbose) { - pb_id <- cli_process_start("Downloading package", msg = binary_file_name) + pb_id <- cli::cli_progress_bar( + format = "{.pkg macrtools}: {.strong Downloading package} {.file {binary_file_name}} {.progress_bar} {.percent} {.spinner}", + format_done = "{.pkg macrtools}: {.strong Download complete} {cli::symbol$tick}", + total = 100 + ) } # Download with progress and error handling - download_start_time <- Sys.time() - download_result <- tryCatch({ + download_start_time <- base::Sys.time() + download_result <- base::tryCatch({ utils::download.file( url, save_location, @@ -165,40 +183,48 @@ binary_download <- function(url, binary_file_name = base::basename(url), verbose ) TRUE }, error = function(e) { - if (verbose) cli_process_done(pb_id, msg = "Download failed") - cli_error(c( - "Failed to download binary package.", - "URL: {.url {url}}", - "Error message: {.val {e$message}}", - "Status code (if available): {.val {attr(e, 'status_code', exact = TRUE) %||% 'Unknown'}}" - ), - advice = "Check your internet connection or try again later. The server may be temporarily unavailable.") + if (verbose) cli::cli_progress_done(pb_id) + + # Extract status code if available + status_code <- base::attr(e, 'status_code', exact = TRUE) + status_info <- if(base::is.null(status_code)) 'Unknown' else status_code + + cli::cli_abort(c( + "x" = "{.pkg macrtools}: Failed to download binary package.", + ">" = "URL: {.url {url}}", + ">" = "Error message: {.val {e$message}}", + ">" = "Status code: {.val {status_info}}", + "i" = "Check your internet connection or try again later. The server may be temporarily unavailable." + )) FALSE }) - download_duration <- difftime(Sys.time(), download_start_time, units = "secs") + download_duration <- base::difftime(base::Sys.time(), download_start_time, units = "secs") if (!download_result) { return(NULL) } if (verbose) { - file_size <- file.info(save_location)$size - file_size_mb <- round(file_size / (1024 * 1024), 2) - cli_process_done(pb_id, msg = "Download complete") - cli_success(c( - "Binary package downloaded successfully.", + file_size <- base::file.info(save_location)$size + file_size_mb <- base::round(file_size / (1024 * 1024), 2) + duration_sec <- base::round(base::as.numeric(download_duration), 1) + avg_speed <- base::round(file_size_mb/(base::as.numeric(download_duration)/60), 2) + + cli::cli_progress_done(pb_id) + cli::cli_alert_success("{.pkg macrtools}: Binary package downloaded successfully.") + cli::cli_bullets(c( "Saved to: {.path {save_location}}", "File size: {.val {file_size_mb}} MB", - "Download time: {.val {round(as.numeric(download_duration), 1)}} seconds", - "Average speed: {.val {round(file_size_mb/(as.numeric(download_duration)/60), 2)}} MB/min" + "Download time: {.val {duration_sec}} seconds", + "Average speed: {.val {avg_speed}} MB/min" )) + cli::cli_text("") # Add spacing } save_location } - #' Install Binary Package in a Tar Format #' #' Unpacks the Tar package and places it into a system library @@ -222,31 +248,39 @@ tar_package_install <- function(path_to_tar, password = NULL, verbose = TRUE) { - install_directory <- shQuote(normalizePath(install_directory)) - binary_file_name <- basename(path_to_tar) + norm_install_dir <- base::shQuote(base::normalizePath(install_directory)) + binary_file_name <- base::basename(path_to_tar) if (verbose) { - cli_info("Installing: {.file {binary_file_name}} into {.path {install_directory}}") + cli::cli_alert_info("{.pkg macrtools}: Installing package from tar archive.") + cli::cli_bullets(c( + "File: {.file {binary_file_name}}", + "Destination: {.path {install_directory}}", + "Strip levels: {.val {strip_levels}}" + )) + cli::cli_text("") # Add spacing } # Step Two: Install the package using tar with stdin redirect - cmd <- paste0("tar fxj ", path_to_tar," -C ", install_directory, " --strip-components ", strip_levels) + cmd <- base::paste0("tar fxj ", path_to_tar," -C ", norm_install_dir, " --strip-components ", strip_levels) status <- shell_execute(cmd, sudo = sudo, password = password, verbose = verbose) # Verify installation is okay: if (status < 0) { - cli_error("Failed to install from {.path {path_to_tar}}") + cli::cli_abort("{.pkg macrtools}: Failed to install from {.path {path_to_tar}}") } if (verbose) { - cli_info("Removing temporary tar file: {.file {binary_file_name}}") + cli::cli_alert_info("{.pkg macrtools}: Removing temporary tar file: {.file {binary_file_name}}") + cli::cli_text("") # Add spacing } # Step Three: Remove the tar package - unlink(path_to_tar) + base::unlink(path_to_tar) if (verbose && status == 0) { - cli_success("Installation completed successfully.") + cli::cli_alert_success("{.pkg macrtools}: Installation completed successfully!") + cli::cli_text("") # Add spacing } status == 0 @@ -259,57 +293,84 @@ dmg_package_install <- function(path_to_dmg, password = NULL, verbose = TRUE) { - volume_with_extension <- basename(path_to_dmg) + volume_with_extension <- base::basename(path_to_dmg) bare_volume <- tools::file_path_sans_ext(volume_with_extension) if (verbose) { - cli_info("Mounting disk image: {.file {volume_with_extension}}") + cli::cli_alert_info("{.pkg macrtools}: Mounting disk image.") + cli::cli_bullets(c( + "Disk image: {.file {volume_with_extension}}", + "Volume name: {.val {bare_volume}}" + )) + cli::cli_text("") # Add spacing } - cmd <- paste("hdiutil attach", shQuote(path_to_dmg), "-nobrowse -quiet") + cmd <- base::paste("hdiutil attach", base::shQuote(path_to_dmg), "-nobrowse -quiet") mount_status <- shell_execute(cmd, sudo = FALSE) if (mount_status != 0) { - cli_error("Failed to mount disk image: {.file {volume_with_extension}}") + cli::cli_abort(c( + "{.pkg macrtools}: Failed to mount disk image.", + "Disk image: {.file {volume_with_extension}}", + "Status code: {.val {mount_status}}" + )) return(FALSE) } if (verbose) { - cli_info("Installing package: {.file {bare_volume}}") + cli::cli_alert_info("{.pkg macrtools}: Installing package from disk image.") + cli::cli_bullets(c( + "Package: {.file {bare_volume}}", + "Package location: {.path {pkg_location_in_dmg}}" + )) + cli::cli_text("") # Add spacing } - cmd <- paste( + cmd <- base::paste( "sudo", "-kS", "installer", "-pkg", - paste0("'/Volumes/", pkg_location_in_dmg, "'"), + base::paste0("'/Volumes/", pkg_location_in_dmg, "'"), "-target", "/" ) install_status <- shell_execute(cmd, sudo = TRUE, password = password) if (install_status != 0) { - cli_error("Failed to install package from disk image.") + cli::cli_abort(c( + "{.pkg macrtools}: Failed to install package from disk image.", + "Disk image: {.file {volume_with_extension}}", + "Status code: {.val {install_status}}" + )) # Attempt to unmount anyway - cmd <- paste("hdiutil", "detach", shQuote(file.path("/Volumes", bare_volume))) + cmd <- base::paste("hdiutil", "detach", base::shQuote(base::file.path("/Volumes", bare_volume))) shell_execute(cmd, sudo = FALSE) return(FALSE) } if (verbose) { - cli_info("Unmounting disk image: {.file {bare_volume}}") + cli::cli_alert_info("{.pkg macrtools}: Unmounting disk image.") + cli::cli_bullets(c( + "Volume: {.file {bare_volume}}" + )) + cli::cli_text("") # Add spacing } - cmd <- paste("hdiutil", "detach", shQuote(file.path("/Volumes", bare_volume))) + cmd <- base::paste("hdiutil", "detach", base::shQuote(base::file.path("/Volumes", bare_volume))) status <- shell_execute(cmd, sudo = FALSE) if (status != 0) { - cli_warning("Failed to unmount disk image. You may need to unmount it manually.") + cli::cli_alert_warning(c( + "{.pkg macrtools}: Failed to unmount disk image.", + "i" = "You may need to unmount it manually." + )) + cli::cli_text("") # Add spacing } if (verbose && install_status == 0) { - cli_success("Installation completed successfully.") + cli::cli_alert_success("{.pkg macrtools}: Installation completed successfully!") + cli::cli_text("") # Add spacing } install_status == 0 @@ -320,14 +381,20 @@ pkg_install <- function(path_to_pkg, password = NULL, verbose = TRUE) { - package_name_with_extension <- basename(path_to_pkg) + package_name_with_extension <- base::basename(path_to_pkg) package_name <- tools::file_path_sans_ext(package_name_with_extension) if (verbose) { - cli_info("Installing package: {.file {package_name}}") + cli::cli_alert_info("{.pkg macrtools}: Installing package.") + cli::cli_bullets(c( + "Package: {.file {package_name}}", + "Path: {.path {path_to_pkg}}", + "Target: {.path {target_location}}" + )) + cli::cli_text("") # Add spacing } - cmd <- paste( + cmd <- base::paste( "sudo", "-kS", "installer", @@ -339,12 +406,17 @@ pkg_install <- function(path_to_pkg, status <- shell_execute(cmd, sudo = TRUE, password = password) if (status != 0) { - cli_error("Failed to install package: {.file {package_name}}") + cli::cli_abort(c( + "{.pkg macrtools}: Failed to install package.", + "Package: {.file {package_name}}", + "Status code: {.val {status}}" + )) return(FALSE) } if (verbose) { - cli_success("Package installation completed successfully.") + cli::cli_alert_success("{.pkg macrtools}: Package installation completed successfully!") + cli::cli_text("") # Add spacing } status == 0 diff --git a/R/macos-versions.R b/R/macos-versions.R deleted file mode 100644 index bd9cc97..0000000 --- a/R/macos-versions.R +++ /dev/null @@ -1,63 +0,0 @@ -# Detect macOS Operating System ---- - -shell_mac_version = function() { - sys::as_text(sys::exec_internal("sw_vers", "-productVersion")$stdout) -} - -is_macos_r_supported = function() { - mac_version = shell_mac_version() - - version_between(mac_version, "10.13.0", "16.0.0") -} - -is_macos_sequoia = function() { - mac_version = shell_mac_version() - - version_between(mac_version, "15.0.0", "16.0.0") -} - -is_macos_sonoma = function() { - mac_version = shell_mac_version() - - version_between(mac_version, "14.0.0", "15.0.0") -} - -is_macos_ventura = function() { - mac_version = shell_mac_version() - - version_between(mac_version, "13.0.0", "14.0.0") -} - -is_macos_monterey = function() { - mac_version = shell_mac_version() - - version_between(mac_version, "12.0.0", "13.0.0") -} - -is_macos_big_sur= function() { - mac_version = shell_mac_version() - - version_between(mac_version, "11.0.0", "12.0.0") -} - -is_macos_catalina = function() { - mac_version = shell_mac_version() - - version_between(mac_version, "10.15.0", "10.16.0") -} - -is_macos_mojave = function() { - mac_version = shell_mac_version() - - version_between(mac_version, "10.14.0", "10.15.0") -} - -is_macos_high_sierra = function() { - mac_version = shell_mac_version() - - version_between(mac_version, "10.13.0", "10.14.0") -} - -is_macos = function() { - system_os() == "darwin" -} diff --git a/R/recipes.R b/R/recipes.R index b37d0f9..fdb0b9d 100644 --- a/R/recipes.R +++ b/R/recipes.R @@ -5,9 +5,6 @@ # # Barely modified for formatting -#' @include shell.R utils.R installers.R -NULL - #' Install Binary Library from the CRAN R macOS Recipes Project #' #' Convenience function that seeks to install pre-built binary libraries @@ -22,12 +19,13 @@ NULL #' @param os.arch Either name of the repository such as `"darwin20/arm64"`, `"darwin20/x86_64"`, `"darwin17/x86_64"`, or `"auto"`. #' Default `"auto"`. #' @param dependencies Install build dependencies (`TRUE`) or only the requested packages (`FALSE`). Default `TRUE`. -#' @param action Determine if the binary should be downloaded and installed (`"install"`) -#' or displayed (`"list"`). Default `"install"` to download and install the binaries. +#' @param action Determine if the binary should be downloaded and installed (`"install"`), +#' displayed (`"list"`), or downloaded but not installed (`"download"`). +#' Default `"install"` to download and install the binaries. #' @param sudo Attempt to install the binaries using `sudo` permissions. #' Default `TRUE`. #' @param password User password to switch into the `sudo` user. Default `NULL`. -#' @param verbose Describe the steps being taken. Default `TRUE` +#' @param verbose Describe the steps being taken. Default `TRUE`. #' @export #' @author #' Simon Urbanek wrote the function and made it available at @@ -48,6 +46,10 @@ NULL #' | [darwin20/x86_64](https://mac.r-project.org/bin/darwin20/x86_64) | /opt/R/x86_64 | macOS 11, Intel (x86_64) | #' | [darwin20/arm64](https://mac.r-project.org/bin/darwin20/arm64) | /opt/R/arm64 | macOS 11, Apple M1 (arm64) | #' +#' @section Differences: +#' The official implementation uses `quiet` as a parameter to suppress output +#' instead of `verbose`. +#' #' @examples #' # Perform a dry-run to see the required development packages. #' recipes_binary_install("r-base-dev", action = "list") @@ -57,57 +59,57 @@ NULL #' recipes_binary_install("r-base-dev", sudo = TRUE) #' } recipes_binary_install = function( - pkgs, - url = "https://mac.R-project.org/bin", - os = tolower( - paste0( - system("uname -s", intern = TRUE), - gsub("\\..*", "", system("uname -r", intern = TRUE)) - ) - ), - arch = system("uname -m", intern = TRUE), - os.arch = "auto", - dependencies = TRUE, - action = c("install", "list"), - sudo = TRUE, - password = NULL, - verbose = TRUE) { + pkgs, + url = "https://mac.R-project.org/bin", + os = base::tolower( + base::paste0( + base::system("uname -s", intern = TRUE), + base::gsub("\\..*", "", base::system("uname -r", intern = TRUE)) + ) + ), + arch = base::system("uname -m", intern = TRUE), + os.arch = "auto", + dependencies = TRUE, + action = c("install", "list", "download"), + sudo = TRUE, + password = NULL, + verbose = TRUE) { up <- function(...) - paste(..., sep = '/') - action <- match.arg(action) + base::paste(..., sep = '/') + action <- base::match.arg(action) if (os.arch == "auto") { rindex <- up(url, "REPOS") - cat("Downloading", rindex, "...\n") - rl <- readLines(u <- url(rindex)) - close(u) - rla <- simplify2array(strsplit(rl[grep("/", rl)], "/")) - rl <- data.frame(os = rla[1, ], arch = rla[2, ]) + if (verbose) base::cat("Downloading", rindex, "...\n") + rl <- base::readLines(u <- base::url(rindex)) + base::close(u) + rla <- base::simplify2array(base::strsplit(rl[base::grep("/", rl)], "/")) + rl <- base::data.frame(os = rla[1, ], arch = rla[2, ]) os.name <- function(os) - gsub("[0-9].*", "", os) + base::gsub("[0-9].*", "", os) os.ver <- function(os) - as.numeric(sub("\\..*", "", gsub("[^0-9]*", "", os))) + base::as.numeric(base::sub("\\..*", "", base::gsub("[^0-9]*", "", os))) rl$os.name <- os.name(rl$os) rl$os.ver <- os.ver(rl$os) rl <- rl[rl$os.name == os.name(os) & rl$os.ver <= os.ver(os), ] - if (nrow(rl) < 1) - stop( + if (base::nrow(rl) < 1) + base::stop( "There is no repository that supports ", os.name(os), " version ", os.ver(os), " or higher.\nAvailable binaries only support: ", - paste(rla[1, ], collapse = ", ") + base::paste(rla[1, ], collapse = ", ") ) - if (!any(rl$arch == arch)) - stop( + if (!base::any(rl$arch == arch)) + base::stop( "Architecture ", arch, " is not supported on os ", @@ -117,38 +119,38 @@ recipes_binary_install = function( ) rl <- rl[rl$arch == arch, ] - rl <- rl[order(rl$os.ver, decreasing = TRUE), ][1, ] + rl <- rl[base::order(rl$os.ver, decreasing = TRUE), ][1, ] - os.arch <- file.path(rl$os, rl$arch) + os.arch <- base::file.path(rl$os, rl$arch) } - cat("Using repository ", up(url, os.arch), "...\n") + if (verbose) base::cat("Using repository ", up(url, os.arch), "...\n") deps <- function(pkgs, db) { ## convert bare (w/o version) names to full names bare <- pkgs %in% db[, "Package"] - if (any(bare)) - pkgs[bare] = rownames(db)[match(pkgs[bare], db[, "Package"])] + if (base::any(bare)) + pkgs[bare] = base::rownames(db)[base::match(pkgs[bare], db[, "Package"])] ## any missing? - mis <- !pkgs %in% rownames(db) - if (any(mis)) - stop("Following binaries have no download candidates: ", - paste(pkgs[mis], collapse = ", ")) + mis <- !pkgs %in% base::rownames(db) + if (base::any(mis)) + base::stop("Following binaries have no download candidates: ", + base::paste(pkgs[mis], collapse = ", ")) dep <- function(pkgs) { - mis <- !pkgs %in% rownames(db) - if (any(mis)) - stop( + mis <- !pkgs %in% base::rownames(db) + if (base::any(mis)) + base::stop( "Following binaries have no download candidates: ", - paste(pkgs[mis], collapse = ", ") + base::paste(pkgs[mis], collapse = ", ") ) nd <- - stats::na.omit(unique(c(pkgs, unlist( - strsplit(db[pkgs, "BuiltWith"], "[, ]+") + stats::na.omit(base::unique(base::c(pkgs, base::unlist( + base::strsplit(db[pkgs, "BuiltWith"], "[, ]+") )))) - if (length(unique(pkgs)) < length(nd)) + if (base::length(base::unique(pkgs)) < base::length(nd)) dep(nd) else nd @@ -160,22 +162,22 @@ recipes_binary_install = function( } pindex <- up(url, os.arch, "PACKAGES") - cat("Downloading index ", pindex, "...\n") - db <- read.dcf(u <- url(pindex)) - close(u) - rownames(db) <- if ("Bundle" %in% colnames(db)) - ifelse(is.na(db[, "Bundle"]), - paste(db[, "Package"], db[, "Version"], sep = '-'), - db[, "Bundle"]) + if (verbose) base::cat("Downloading index ", pindex, "...\n") + db <- base::read.dcf(u <- base::url(pindex)) + base::close(u) + base::rownames(db) <- if ("Bundle" %in% base::colnames(db)) + base::ifelse(base::is.na(db[, "Bundle"]), + base::paste(db[, "Package"], db[, "Version"], sep = '-'), + db[, "Bundle"]) else - paste(db[, "Package"], db[, "Version"], sep = '-') + base::paste(db[, "Package"], db[, "Version"], sep = '-') - if (identical(pkgs, "all")) + if (base::identical(pkgs, "all")) pkgs <- stats::na.omit(db[, "Package"]) need <- deps(pkgs, db) ## remove bundles as they have no binary - if ("Bundle" %in% colnames(db) && - any(rem <- need %in% stats::na.omit(db[, "Bundle"]))) + if ("Bundle" %in% base::colnames(db) && + base::any(rem <- need %in% stats::na.omit(db[, "Bundle"]))) need <- need[!rem] urls <- up(url, os.arch, db[need, "Binary"]) @@ -183,11 +185,11 @@ recipes_binary_install = function( # Check if we're using sudo & have a password entered_recipes_password = password - if (sudo && is.null(entered_recipes_password)) + if (sudo && base::is.null(entered_recipes_password)) entered_recipes_password = askpass::askpass() # Determine the correct installation path based on arch type - supplied_arch = strsplit(os.arch, "/")[[1]][2] + supplied_arch = base::strsplit(os.arch, "/")[[1]][2] installation_directory = recipe_binary_install_location(supplied_arch) installation_strip_level = recipe_binary_install_strip_level(supplied_arch) @@ -208,8 +210,17 @@ recipes_binary_install = function( verbose = verbose) } - return(invisible(TRUE)) + return(base::invisible(TRUE)) + } else if (action == "download") { + for (u in urls) { + if (!base::file.exists(base::basename(u))) { + + if (verbose) base::cat("Downloading ", u, "...\n", sep='') + + if (base::system(base::paste("curl", "-sSLO", base::shQuote(u))) < 0) + base::stop("Failed to download ", u) + } + } } else urls } - diff --git a/R/renviron.R b/R/renviron.R index b74c6c5..86cb508 100644 --- a/R/renviron.R +++ b/R/renviron.R @@ -1,28 +1,25 @@ -#' @include blocks.R -NULL - use_r_environ = function(option, value, path = "~/.Renviron", block_start = "## macrtools: start", block_end = "## macrtools: stop") { - if (!file.exists(path)) { - message("`", path ,"` file not found at location.") - message("Creating a new Renviron file at: ~/.Renviron") - file.create(path) + if (!base::file.exists(path)) { + base::message("`", path ,"` file not found at location.") + base::message("Creating a new Renviron file at: ~/.Renviron") + base::file.create(path) } changed = block_append( - desc = paste0("setting of ", option, "=", value), - value = paste0(option, "=", value), + desc = base::paste0("setting of ", option, "=", value), + value = base::paste0(option, "=", value), path = path, block_start = block_start, block_end = block_end ) if (changed) { - message("Please restart R for the new startup settings to take effect") + base::message("Please restart R for the new startup settings to take effect") } - invisible(changed) + base::invisible(changed) } diff --git a/R/shell.R b/R/shell.R index f2805ce..5a79dcf 100644 --- a/R/shell.R +++ b/R/shell.R @@ -1,6 +1,3 @@ -#' @include cli-custom.R -NULL - #' Execute shell commands #' #' Functions to execute shell commands with or without sudo @@ -11,11 +8,13 @@ NULL #' @keywords internal shell_command <- function(cmd, verbose = TRUE) { if (verbose) { - cli_info(c( - "Executing shell command.", + working_dir <- base::getwd() + cli::cli_alert_info("{.pkg macrtools}: Executing shell command.") + cli::cli_bullets(c( "Command: {.code {cmd}}", - "Working directory: {.path {getwd()}}" + "Working directory: {.path {working_dir}}" )) + cli::cli_text("") # Add spacing } base::system(cmd) } @@ -29,26 +28,29 @@ shell_command <- function(cmd, verbose = TRUE) { #' @return The exit status of the command (0 for success) #' @keywords internal shell_sudo_command <- function(cmd, password, verbose = TRUE, prefix = "sudo -kS ") { - cmd_with_sudo <- paste0(prefix, cmd) + cmd_with_sudo <- base::paste0(prefix, cmd) if (verbose) { - cli_info(c( - "Executing privileged command with sudo.", + cli::cli_alert_info("{.pkg macrtools}: Executing privileged command with sudo.") + cli::cli_bullets(c( "Command: {.code {cmd_with_sudo}}", "Administrative privileges will be required." )) + cli::cli_text("") # Add spacing } - if (is.null(password)) { - result <- base::system(cmd_with_sudo, input = askpass::askpass("Please enter your administrator password:")) + result <- if (base::is.null(password)) { + base::system(cmd_with_sudo, input = askpass::askpass("Please enter your administrator password:")) } else { - result <- base::system(cmd_with_sudo, input = password) + base::system(cmd_with_sudo, input = password) } if (verbose && result != 0) { - cli_warning(c( - "Command execution failed with status: {.val {result}}.", + cli::cli_alert_warning(c( + "{.pkg macrtools}: Command execution failed.", + "Status code: {.val {result}}", "This might indicate permission issues or syntax errors." )) + cli::cli_text("") # Add spacing } return(result) @@ -66,27 +68,32 @@ shell_sudo_command <- function(cmd, password, verbose = TRUE, prefix = "sudo -kS #' @return The exit status of the command (0 for success) #' @keywords internal shell_execute <- function(cmd, sudo = FALSE, password = NULL, verbose = TRUE, timeout = 300) { - command_start_time <- Sys.time() + command_start_time <- base::Sys.time() result <- if (sudo) { - shell_sudo_command(cmd = cmd, password = password, verbose = verbose) + shell_sudo_command(cmd = cmd, password = password, verbose = FALSE) } else { - shell_command(cmd = cmd, verbose = verbose) + shell_command(cmd = cmd, verbose = FALSE) } - command_duration <- difftime(Sys.time(), command_start_time, units = "secs") + command_duration <- base::difftime(base::Sys.time(), command_start_time, units = "secs") + duration_seconds <- base::round(base::as.numeric(command_duration), 2) if (verbose) { if (result == 0) { - cli_info(c( - "Command completed successfully in {.val {round(as.numeric(command_duration), 2)}} seconds.", + cli::cli_alert_success("{.pkg macrtools}: Command completed successfully.") + cli::cli_bullets(c( + "Execution time: {.val {duration_seconds}} seconds", "Exit status: {.val {result}}" )) + cli::cli_text("") # Add spacing } else { - cli_warning(c( - "Command completed with non-zero exit status in {.val {round(as.numeric(command_duration), 2)}} seconds.", + cli::cli_alert_warning("{.pkg macrtools}: Command completed with non-zero exit status.") + cli::cli_bullets(c( + "Execution time: {.val {duration_seconds}} seconds", "Exit status: {.val {result}}" )) + cli::cli_text("") # Add spacing } } diff --git a/R/system-checks.R b/R/system-checks.R deleted file mode 100644 index bf9fa49..0000000 --- a/R/system-checks.R +++ /dev/null @@ -1,17 +0,0 @@ -# System functions ---- -system_os = function() { - tolower(Sys.info()[["sysname"]]) -} - -system_arch = function() { - R.version$arch -} - -# Architecture Checks ---- -is_aarch64 = function() { - system_arch() == "aarch64" -} - -is_x86_64 = function() { - system_arch() == "x86_64" -} diff --git a/R/system.R b/R/system.R new file mode 100644 index 0000000..afbdbe9 --- /dev/null +++ b/R/system.R @@ -0,0 +1,173 @@ +#' System OS Detection +#' +#' @return The name of the operating system in lowercase +#' @keywords internal +system_os <- function() { + base::tolower(base::Sys.info()[["sysname"]]) +} + +#' System Architecture Detection +#' +#' @return The system architecture identifier +#' @keywords internal +system_arch <- function() { + base::R.version$arch +} + +#' Check if System is aarch64 +#' +#' @return TRUE if system is Apple Silicon (M-series) Mac, FALSE otherwise +#' @keywords internal +is_aarch64 <- function() { + system_arch() == "aarch64" +} + +#' Check if System is x86_64 +#' +#' @return TRUE if system is Intel-based Mac, FALSE otherwise +#' @keywords internal +is_x86_64 <- function() { + system_arch() == "x86_64" +} + +#' Check if System is macOS +#' +#' @return TRUE if system is macOS, FALSE otherwise +#' @keywords internal +is_macos <- function() { + system_os() == "darwin" +} + +#' Get macOS Version +#' +#' @return The macOS version string +#' @keywords internal +shell_mac_version <- function() { + sys::as_text(sys::exec_internal("sw_vers", "-productVersion")$stdout) +} + +#' Check if macOS Version is Supported for R +#' +#' @return TRUE if macOS version is supported, FALSE otherwise +#' @keywords internal +is_macos_r_supported <- function() { + mac_version <- shell_mac_version() + version_between(mac_version, "10.13.0", "16.0.0") +} + +#' Check if macOS Sequoia +#' +#' @return TRUE if system is macOS Sequoia, FALSE otherwise +#' @keywords internal +is_macos_sequoia <- function() { + mac_version <- shell_mac_version() + version_between(mac_version, "15.0.0", "16.0.0") +} + +#' Check if macOS Sonoma +#' +#' @return TRUE if system is macOS Sonoma, FALSE otherwise +#' @keywords internal +is_macos_sonoma <- function() { + mac_version <- shell_mac_version() + version_between(mac_version, "14.0.0", "15.0.0") +} + +#' Check if macOS Ventura +#' +#' @return TRUE if system is macOS Ventura, FALSE otherwise +#' @keywords internal +is_macos_ventura <- function() { + mac_version <- shell_mac_version() + version_between(mac_version, "13.0.0", "14.0.0") +} + +#' Check if macOS Monterey +#' +#' @return TRUE if system is macOS Monterey, FALSE otherwise +#' @keywords internal +is_macos_monterey <- function() { + mac_version <- shell_mac_version() + version_between(mac_version, "12.0.0", "13.0.0") +} + +#' Check if macOS Big Sur +#' +#' @return TRUE if system is macOS Big Sur, FALSE otherwise +#' @keywords internal +is_macos_big_sur <- function() { + mac_version <- shell_mac_version() + version_between(mac_version, "11.0.0", "12.0.0") +} + +#' Check if macOS Catalina +#' +#' @return TRUE if system is macOS Catalina, FALSE otherwise +#' @keywords internal +is_macos_catalina <- function() { + mac_version <- shell_mac_version() + version_between(mac_version, "10.15.0", "10.16.0") +} + +#' Check if macOS Mojave +#' +#' @return TRUE if system is macOS Mojave, FALSE otherwise +#' @keywords internal +is_macos_mojave <- function() { + mac_version <- shell_mac_version() + version_between(mac_version, "10.14.0", "10.15.0") +} + +#' Check if macOS High Sierra +#' +#' @return TRUE if system is macOS High Sierra, FALSE otherwise +#' @keywords internal +is_macos_high_sierra <- function() { + mac_version <- shell_mac_version() + version_between(mac_version, "10.13.0", "10.14.0") +} + +#' Check if Version is Above Threshold +#' +#' @param software_version Version string to check +#' @param than Threshold version to compare against +#' @return TRUE if software_version is above than, FALSE otherwise +#' @keywords internal +version_above <- function(software_version, than) { + utils::compareVersion(software_version, than) == 1L +} + +#' Check if Version is Between Bounds +#' +#' @param software_version Version string to check +#' @param lower Lower bound for version check (inclusive) +#' @param greater_strict Upper bound for version check (exclusive) +#' @return TRUE if software_version is between bounds, FALSE otherwise +#' @keywords internal +version_between <- function(software_version, lower, greater_strict) { + above <- utils::compareVersion(software_version, lower) %in% c(0L, 1L) + below <- utils::compareVersion(software_version, greater_strict) %in% c(-1L) + above && below +} + +#' Check R Version +#' +#' @param target_version Target R version to check against (e.g., "4.0") +#' @param compare_major_minor Whether to compare only major.minor (TRUE) or major.minor.patch (FALSE) +#' @return TRUE if R version matches target_version, FALSE otherwise +#' @keywords internal +is_r_version <- function(target_version, compare_major_minor = TRUE) { + minor_value <- if (compare_major_minor) { + # If x.y.z, this retrieves y + base::strsplit(base::R.version$minor, ".", fixed = TRUE)[[1]][1] + } else { + # If x.y.z, this retrieves y.z + base::R.version$minor + } + + # Build the version string of x.y or x.y.z + version_string <- base::paste(base::R.version$major, minor_value, sep = ".") + + # Check for equality. + return(version_string == target_version) +} diff --git a/R/toolchain.R b/R/toolchain.R index 186100d..3a76afc 100644 --- a/R/toolchain.R +++ b/R/toolchain.R @@ -1,6 +1,3 @@ -#' @include xcode-cli.R gfortran.R recipes.R cli-custom.R -NULL - #' Install and Uninstall the macOS R Toolchain #' #' The `macos_rtools_install()` function aims to install all required dependencies @@ -48,241 +45,307 @@ NULL #' #' } macos_rtools_install <- function( - password = getOption("macrtools.password"), + password = base::getOption("macrtools.password"), verbose = TRUE ) { - # Initial system check assert_mac() assert_macos_supported() assert_r_version_supported() - cli_system_update("Installing macOS R Development Toolchain", c( - "This process will install all components required for R package development on macOS:", + # Installation heading and components list - proper formatting + cli::cli_h3("Installing macOS R Development Toolchain") + cli::cli_text("This process will install all required components:") + cli::cli_ul(c( "Xcode Command Line Tools - Apple's development utilities", "gfortran - GNU Fortran compiler required for many scientific packages", "r-base-dev - Essential libraries from the R-macOS Recipes project" )) + cli::cli_text("") # Add spacing + + # System information with temporary variables + os_version <- shell_mac_version() + os_release <- base::Sys.info()['release'] + arch <- system_arch() + r_version <- base::paste(base::R.version$major, base::R.version$minor, sep='.') + disk_space <- base::tryCatch( + base::round(base::as.numeric(base::system('df -h / | tail -1 | awk \'{print $4}\'', intern=TRUE)), 2), + error = function(e) "Unknown" + ) - cli_info(c( - "System requirements check:", - "Operating system: {.val {shell_mac_version()}} ({.val {Sys.info()['release']}})", - "Architecture: {.val {system_arch()}}", - "R version: {.val {paste(R.version$major, R.version$minor, sep='.')}}", - "Available disk space: {.val {round(as.numeric(system('df -h / | tail -1 | awk \'{print $4}\'', intern=TRUE)), 2)}} GB", + cli::cli_alert_info("System requirements check:") + cli::cli_bullets(c( + "Operating system: {.val {os_version}} ({.val {os_release}})", + "Architecture: {.val {arch}}", + "R version: {.val {r_version}}", + "Available disk space: {.val {disk_space}} GB", "Administrator privileges: Required" )) + cli::cli_text("") # Add spacing - cli_info(c( - "Installation prerequisites:", + cli::cli_alert_info("Installation prerequisites:") + cli::cli_bullets(c( "Ensure you have a stable internet connection", "Connect your computer to a power source", "Estimated installation time: 15-25 minutes", "Required disk space: Approximately 5-7 GB" )) + cli::cli_text("") # Add spacing entered_password <- password if(base::is.null(entered_password)) { - cli_info("Administrative privileges are required for installation.") + cli::cli_alert_info("Administrative privileges are required for installation.") entered_password <- askpass::askpass("Please enter your administrator password:") } - describe_steps <- isTRUE(verbose) + describe_steps <- base::isTRUE(verbose) # Create a detailed progress bar if (verbose) { - pb_id <- cli_process_start("Installing R development toolchain", total = 100) - cli_info("Installation process started at: {.val {format(Sys.time(), '%Y-%m-%d %H:%M:%S')}}") + pb_id <- cli::cli_progress_bar( + format = "{.pkg macrtools}: {.strong Installing R development toolchain} {.progress_bar} {.percent} {.spinner}", + format_done = "{.pkg macrtools}: {.strong Installation complete} {cli::symbol$tick}", + total = 100 + ) + + current_time <- base::format(base::Sys.time(), '%Y-%m-%d %H:%M:%S') + cli::cli_alert_info("Installation process started at: {.val {current_time}}") + cli::cli_text("") # Add spacing } # COMPONENT 1: Xcode Command Line Tools - cli_system_update("Component 1 of 3: Xcode Command Line Tools", c( - "Apple's development utilities providing compilers and build tools", + cli::cli_h3("Component 1 of 3: Xcode Command Line Tools") + cli::cli_text("Apple's development utilities providing compilers and build tools") + cli::cli_ul(c( "Includes: gcc, make, git, clang, and other essential development tools", "Location: /Library/Developer/CommandLineTools", "Size: ~1.5 GB" )) + cli::cli_text("") # Add spacing # Step 1: Xcode CLI result_xcode <- TRUE if(!is_xcode_app_installed()) { if(!is_xcode_cli_installed()) { if (verbose) { - cli_process_update(pb_id, 0.1, "Checking Xcode CLI requirements") - cli_info(c( - "Need to install Xcode Command Line Tools.", + cli::cli_progress_update(pb_id, 0.1) + cli::cli_alert_info("{.pkg macrtools}: Need to install Xcode Command Line Tools.") + cli::cli_bullets(c( "Source: Apple Software Update", "Installation method: softwareupdate command", "Status: Not installed", "Estimated time: 5-10 minutes" )) + cli::cli_text("") # Add spacing } - if (verbose) cli_process_update(pb_id, 0.2, "Installing Xcode CLI") + if (verbose) cli::cli_progress_update(pb_id, 0.2) result_xcode <- xcode_cli_install(password = entered_password, verbose = describe_steps) if(!result_xcode) { - cli_error(c( - "Failed to install Xcode Command Line Tools.", + cli::cli_abort(c( + "{.pkg macrtools}: Failed to install Xcode Command Line Tools.", "This is a required component for R package development.", - "Installation status: Failed" - ), - advice = "Try installing manually by running 'xcode-select --install' in Terminal.") + "Installation status: Failed", + "i" = "Try installing manually by running 'xcode-select --install' in Terminal." + )) } - if (verbose) cli_process_update(pb_id, 0.3, "Xcode CLI installation complete") + if (verbose) cli::cli_progress_update(pb_id, 0.3) } else { if(describe_steps) { - cli_info(c( - "Xcode Command Line Tools already installed.", + # Get Xcode CLI version information + xcode_version <- base::tryCatch( + sys::as_text(sys::exec_internal('xcode-select', '--version')$stdout), + error = function(e) 'Unknown' + ) + + cli::cli_alert_info("{.pkg macrtools}: Xcode Command Line Tools already installed.") + cli::cli_bullets(c( "Location: {.path {xcode_cli_path()}}", - "Version: {.val {tryCatch(sys::as_text(sys::exec_internal('xcode-select', '--version')$stdout), error = function(e) 'Unknown')}}", + "Version: {.val {xcode_version}}", "Status: Pre-installed, no action needed" )) + cli::cli_text("") # Add spacing } - if (verbose) cli_process_update(pb_id, 0.3, "Xcode CLI already installed") + if (verbose) cli::cli_progress_update(pb_id, 0.3) } } else { if(describe_steps) { - xcode_app_info <- tryCatch(sys::as_text(sys::exec_internal('xcodebuild', '-version')$stdout), error = function(e) "Unknown") - cli_info(c( - "Full Xcode.app IDE is installed.", + # Get full Xcode app version information + xcode_app_info <- base::tryCatch( + sys::as_text(sys::exec_internal('xcodebuild', '-version')$stdout), + error = function(e) "Unknown" + ) + + cli::cli_alert_info("{.pkg macrtools}: Full Xcode.app IDE is installed.") + cli::cli_bullets(c( "Location: {.path {'/Applications/Xcode.app'}}", "Version information: {.val {xcode_app_info}}", "Status: Pre-installed, skipping Command Line Tools installation" )) + cli::cli_text("") # Add spacing } - if (verbose) cli_process_update(pb_id, 0.3, "Using existing Xcode.app") + if (verbose) cli::cli_progress_update(pb_id, 0.3) } # COMPONENT 2: GNU Fortran Compiler - cli_system_update("Component 2 of 3: GNU Fortran Compiler", c( - "Essential compiler for scientific computing and many CRAN packages", + cli::cli_h3("Component 2 of 3: GNU Fortran Compiler") + cli::cli_text("Essential compiler for scientific computing and many CRAN packages") + cli::cli_ul(c( "Provides: Fortran compiler compatible with R's build tools", - paste0("Location: ", gfortran_install_location(), "/gfortran"), + "Location: {gfortran_install_location()}/gfortran", "Size: ~1 GB" )) + cli::cli_text("") # Add spacing # Step 2: gfortran - if (verbose) cli_process_update(pb_id, 0.4, "Checking gfortran requirements") + if (verbose) cli::cli_progress_update(pb_id, 0.4) result_gfortran <- TRUE if(!is_gfortran_installed()) { if (verbose) { - cli_info(c( - "Need to install GNU Fortran compiler.", + cli::cli_alert_info("{.pkg macrtools}: Need to install GNU Fortran compiler.") + cli::cli_bullets(c( "Source: R-Project macOS tools repository", - "Architecture: {.val {system_arch()}}", - "R version: {.val {paste(R.version$major, R.version$minor, sep='.')}}", + "Architecture: {.val {arch}}", + "R version: {.val {r_version}}", "Status: Not installed", "Estimated time: 2-5 minutes" )) + cli::cli_text("") # Add spacing } - if (verbose) cli_process_update(pb_id, 0.5, "Installing gfortran") + if (verbose) cli::cli_progress_update(pb_id, 0.5) result_gfortran <- gfortran_install(password = entered_password, verbose = describe_steps) if(!result_gfortran) { - cli_error(c( - "Failed to install GNU Fortran compiler.", + cli::cli_abort(c( + "{.pkg macrtools}: Failed to install GNU Fortran compiler.", "This is a required component for many scientific R packages.", - "Installation status: Failed" - ), - advice = "Try installing manually following the instructions at: https://mac.thecoatlessprofessor.com/macrtools/reference/gfortran.html") + "Installation status: Failed", + "i" = "Try installing manually following the instructions at: https://mac.thecoatlessprofessor.com/macrtools/reference/gfortran.html" + )) } - if (verbose) cli_process_update(pb_id, 0.6, "Gfortran installation complete") + if (verbose) cli::cli_progress_update(pb_id, 0.6) } else { if(describe_steps) { - cli_info(c( - "GNU Fortran compiler already installed.", - "Location: {.path {file.path(gfortran_install_location(), 'gfortran')}}", - "Version: {.val {tryCatch(sys::as_text(sys::exec_internal('gfortran', '--version')$stdout), error = function(e) 'Unknown')}}", + # Get gfortran version information + gfortran_version_info <- base::tryCatch( + sys::as_text(sys::exec_internal('gfortran', '--version')$stdout), + error = function(e) 'Unknown' + ) + + install_path <- base::file.path(gfortran_install_location(), 'gfortran') + + cli::cli_alert_info("{.pkg macrtools}: GNU Fortran compiler already installed.") + cli::cli_bullets(c( + "Location: {.path {install_path}}", + "Version: {.val {gfortran_version_info}}", "Status: Pre-installed, no action needed" )) + cli::cli_text("") # Add spacing } - if (verbose) cli_process_update(pb_id, 0.6, "Gfortran already installed") + if (verbose) cli::cli_progress_update(pb_id, 0.6) } # COMPONENT 3: R Development Libraries - cli_system_update("Component 3 of 3: R Development Libraries", c( - "Essential third-party libraries required for R package development", + cli::cli_h3("Component 3 of 3: R Development Libraries") + cli::cli_text("Essential third-party libraries required for R package development") + cli::cli_ul(c( "Provides: zlib, libbz2, liblzma, pcre2, and other dependencies", - paste0("Location: ", recipe_binary_install_location(system_arch())), + "Location: {recipe_binary_install_location(arch)}", "Size: ~2-3 GB" )) + cli::cli_text("") # Add spacing # Step 3: r-base-dev if (verbose) { - cli_process_update(pb_id, 0.7, "Installing R development libraries") - cli_info(c( - "Installing R development libraries from the R-macOS recipes project.", + cli::cli_progress_update(pb_id, 0.7) + cli::cli_alert_info("{.pkg macrtools}: Installing R development libraries.") + cli::cli_bullets(c( "Source: R-Project macOS binary repository", - "Target: {.path {recipe_binary_install_location(system_arch())}}", - "Architecture: {.val {system_arch()}}", + "Target: {.path {recipe_binary_install_location(arch)}}", + "Architecture: {.val {arch}}", "Status: Installation starting", "Estimated time: 5-10 minutes" )) + cli::cli_text("") # Add spacing } result_base_dev <- recipes_binary_install( "r-base-dev", sudo = TRUE, password = entered_password, verbose = verbose ) - if (verbose) cli_process_update(pb_id, 0.9, "Development libraries installation complete") + if (verbose) cli::cli_progress_update(pb_id, 0.9) # Finalize installation if (verbose) { - cli_process_update(pb_id, 1.0, "Installation complete") - cli_process_done(pb_id) + cli::cli_progress_update(pb_id, 1.0) + cli::cli_progress_done(pb_id) } rtools_install_clean <- result_gfortran && result_xcode && base::isTRUE(result_base_dev) if(rtools_install_clean) { - # Generate system report - r_version <- base::paste(R.version$major, R.version$minor, sep='.') - xcode_info <- base::tryCatch(sys::as_text(sys::exec_internal('xcode-select', '--version')$stdout), error = function(e) "Unknown") - gfortran_info <- base::tryCatch(sys::as_text(sys::exec_internal('gfortran', '--version')$stdout), error = function(e) "Unknown") - - cli_system_update("Installation Summary: Success", c( + # Get component information for summary + xcode_info <- base::tryCatch( + sys::as_text(sys::exec_internal('xcode-select', '--version')$stdout), + error = function(e) "Unknown" + ) + gfortran_info <- base::tryCatch( + sys::as_text(sys::exec_internal('gfortran', '--version')$stdout), + error = function(e) "Unknown" + ) + + # If version info is too long, truncate it + xcode_summary <- base::substr(xcode_info, 1, 20) + gfortran_summary <- base::substr(gfortran_info, 1, 20) + if(base::nchar(xcode_info) > 20) xcode_summary <- base::paste0(xcode_summary, "...") + if(base::nchar(gfortran_info) > 20) gfortran_summary <- base::paste0(gfortran_summary, "...") + + cli::cli_h3("Installation Summary: Success") + cli::cli_ul(c( "Xcode Command Line Tools installed", "GNU Fortran Compiler installed", "R Development Libraries installed" )) - - cli_success(c( - "macOS R development toolchain has been successfully installed!", - "", - "System configuration:", - "macOS version: {.val {shell_mac_version()}}", - "Architecture: {.val {system_arch()}}", + cli::cli_text("") # Add spacing + + cli::cli_alert_success("{.pkg macrtools}: macOS R development toolchain has been successfully installed!") + cli::cli_text("") # Add spacing + cli::cli_text("System configuration:") + cli::cli_bullets(c( + "macOS version: {.val {os_version}}", + "Architecture: {.val {arch}}", "R version: {.val {r_version}}", - "Xcode tools: {.val {base::substr(xcode_info, 1, 20)}}...", - "Fortran: {.val {base::substr(gfortran_info, 1, 20)}}...", - "", - "Your system is now configured for R package development.", - "You can install packages from source with: {.code install.packages('package_name', type = 'source')}" - )) - - cli_info(c( - "Installation completed at: {.val {format(Sys.time(), '%Y-%m-%d %H:%M:%S')}}", - "If you encounter issues with package installation, run:", - "{.code pkgbuild::check_build_tools(debug = TRUE)}" + "Xcode tools: {.val {xcode_summary}}", + "Fortran: {.val {gfortran_summary}}" )) + cli::cli_text("") # Add spacing + cli::cli_text("Your system is now configured for R package development.") + cli::cli_text("You can install packages from source with: {.code install.packages('package_name', type = 'source')}") + cli::cli_text("") # Add spacing + + current_time <- base::format(base::Sys.time(), '%Y-%m-%d %H:%M:%S') + cli::cli_alert_info("Installation completed at: {.val {current_time}}") + cli::cli_text("If you encounter issues with package installation, run:") + cli::cli_code("pkgbuild::check_build_tools(debug = TRUE)") } else { - cli_error(c( - "Installation failed. Some components could not be installed properly.", + cli::cli_abort(c( + "{.pkg macrtools}: Installation failed. Some components could not be installed properly.", "Xcode CLI: {.val {if(result_xcode) 'Success' else 'Failed'}}", "Gfortran: {.val {if(result_gfortran) 'Success' else 'Failed'}}", "R development libraries: {.val {if(base::isTRUE(result_base_dev)) 'Success' else 'Failed'}}", "", - "Please check the error messages above for more details." - ), - advice = "Try running the installation again or installing each component separately.") + "Please check the error messages above for more details.", + "i" = "Try running the installation again or installing each component separately." + )) } base::invisible(rtools_install_clean) } + #' @rdname macos-rtools #' @export #' @examples @@ -297,65 +360,83 @@ macos_rtools_install <- function( #' #' } macos_rtools_uninstall <- function( - password = getOption("macrtools.password"), + password = base::getOption("macrtools.password"), verbose = TRUE ) { - cli_info(c( - "Uninstalling Xcode CLI and gfortran...", - "Please ensure you are connected to a power source." + cli::cli_alert_info("{.pkg macrtools}: Beginning uninstallation process.") + cli::cli_bullets(c( + "Components to remove: Xcode CLI and gfortran", + "Please ensure you are connected to a power source" )) + cli::cli_text("") # Add spacing - if(is.null(password)) { - cli_info("Please enter your password to continue.") - password <- askpass::askpass() + if(base::is.null(password)) { + cli::cli_alert_info("{.pkg macrtools}: Administrative privileges required.") + password <- askpass::askpass("Please enter your password to continue:") } # Create a progress bar if (verbose) { - pb_id <- cli_process_start("Uninstalling R development toolchain") + pb_id <- cli::cli_progress_bar( + format = "{.pkg macrtools}: {.strong Uninstalling R development toolchain} {.progress_bar} {.percent} {.spinner}", + format_done = "{.pkg macrtools}: {.strong Uninstallation complete} {cli::symbol$tick}", + total = 100 + ) } # Step 1: Uninstall Xcode CLI result_xcode <- TRUE if(is_xcode_cli_installed()) { if (verbose) { - cli_process_update(pb_id, 0.3) - cli_info("Uninstalling Xcode CLI...") + cli::cli_progress_update(pb_id, 0.3) + cli::cli_alert_info("{.pkg macrtools}: Uninstalling Xcode CLI...") + cli::cli_text("") # Add spacing } result_xcode <- xcode_cli_uninstall(password = password, verbose = verbose) if(!result_xcode) { - cli_error("Failed to uninstall Xcode CLI. Please see manual steps.") + cli::cli_abort("{.pkg macrtools}: Failed to uninstall Xcode CLI. Please see manual steps.") } } else { - if(verbose) cli_info("Xcode CLI was not installed, skipping uninstall procedure.") + if(verbose) { + cli::cli_alert_info("{.pkg macrtools}: Xcode CLI was not installed, skipping uninstall procedure.") + cli::cli_text("") # Add spacing + } } # Step 2: Uninstall gfortran - if (verbose) cli_process_update(pb_id, 0.7) + if (verbose) cli::cli_progress_update(pb_id, 0.7) result_gfortran <- TRUE if(is_gfortran_installed()) { - if (verbose) cli_info("Uninstalling gfortran...") + if (verbose) { + cli::cli_alert_info("{.pkg macrtools}: Uninstalling gfortran...") + cli::cli_text("") # Add spacing + } result_gfortran <- gfortran_uninstall(password = password, verbose = verbose) if(!result_gfortran) { - cli_error("Failed to uninstall gfortran. Please see manual steps.") + cli::cli_abort("{.pkg macrtools}: Failed to uninstall gfortran. Please see manual steps.") } } else { - if(verbose) cli_info("gfortran was not installed, skipping uninstall procedure.") + if(verbose) { + cli::cli_alert_info("{.pkg macrtools}: gfortran was not installed, skipping uninstall procedure.") + cli::cli_text("") # Add spacing + } } if (verbose) { - cli_process_update(pb_id, 1.0) - cli_process_done(pb_id) + cli::cli_progress_update(pb_id, 1.0) + cli::cli_progress_done(pb_id) } clean <- result_gfortran && result_xcode if(clean) { - cli_success(c( - "Xcode CLI and Gfortran have been successfully removed from your system.", - "Note: This did not uninstall any binaries from the recipes project." + cli::cli_alert_success("{.pkg macrtools}: Uninstallation complete.") + cli::cli_bullets(c( + "Xcode CLI: Successfully removed", + "Gfortran: Successfully removed", + "Note: This did not uninstall any binaries from the recipes project" )) } - invisible(clean) + base::invisible(clean) } diff --git a/R/utils.R b/R/utils.R index 4bb3227..e753663 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,8 +1,3 @@ -#' @include cli-custom.R -NULL - -# Custom CLI Printing ----- - #' Print CLI Responses #' @param x An object with class `cli` #' @param ... Additional parameters @@ -20,22 +15,32 @@ print.cli <- function(x, ...) { cli::cli_h1("Status") status_color <- if(x$status == 0) "{.green}" else "{.red}" - cli::cli_text(base::paste0(status_color, x$status, "{.reset}")) + cli::cli_text("{status_color}{x$status}{.reset}") return(base::invisible(x)) } -# Display download warning ---- +#' Verify Status of Operation +#' +#' @param status Status code from operation +#' @param program Name of the program being installed or uninstalled +#' @param url Optional URL for manual instructions +#' @param type Type of operation ("uninstall" or "install") +#' @return TRUE if status is successful, FALSE otherwise (invisibly) +#' @keywords internal verify_status <- function(status, program, url, type = c("uninstall", "install")) { type <- base::match.arg(type) if(base::isFALSE(status)) { - cli_error(c( - "Operation failed: Could not {type} {.pkg {program}}.", - "Status: {.val {status}}", - "Operation type: {.val {type}}", - "Time of failure: {.val {base::format(base::Sys.time(), '%Y-%m-%d %H:%M:%S')}}", - if(!base::missing(url)) c("Manual instructions available at:", "{.url {url}}") + time_of_failure <- base::format(base::Sys.time(), '%Y-%m-%d %H:%M:%S') + url_info <- if(!base::missing(url)) c("Manual instructions available at:", "{.url {url}}") else NULL + + cli::cli_abort(c( + "{.pkg macrtools}: Operation failed: Could not {type} {.pkg {program}}.", + "{.pkg macrtools}: Status: {.val {status}}", + "{.pkg macrtools}: Operation type: {.val {type}}", + "{.pkg macrtools}: Time of failure: {.val {time_of_failure}}", + url_info ), advice = base::paste0("You may need to run this operation with administrative privileges or check for system compatibility issues.")) return(base::invisible(FALSE)) @@ -44,15 +49,21 @@ verify_status <- function(status, program, url, type = c("uninstall", "install") base::invisible(TRUE) } -# Obtain a password if not present ---- +#' Force Password Entry if Not Provided +#' +#' @param supplied_password Password provided by user (may be NULL) +#' @return Password to use for operations +#' @keywords internal force_password <- function(supplied_password) { entered_password <- supplied_password if(base::is.null(entered_password)) { - cli_info(c( - "Administrative privileges required.", + current_user <- base::Sys.info()['user'] + + cli::cli_alert_info(c( + "{.pkg macrtools}: Administrative privileges required.", "Your user account password is needed to execute privileged operations.", "This password will not be stored and is only used for the current session.", - "Current user: {.val {base::Sys.info()['user']}}" + "Current user: {.val {current_user}}" )) entered_password <- askpass::askpass("Please enter your administrator password:") } @@ -60,12 +71,22 @@ force_password <- function(supplied_password) { entered_password } -# Get caller environment ---- +#' Get Caller Environment +#' +#' @param n Number of frames to go back +#' @return Caller environment +#' @keywords internal caller_env <- function(n = 1) { base::parent.frame(n + 1) } -# Helpful null coalesce operator +#' Null Coalesce Operator +#' +#' @param x First value (may be NULL) +#' @param y Default value if x is NULL +#' @return x if not NULL, otherwise y +#' @name null_coalesce +#' @keywords internal `%||%` <- function(x, y) { if (base::is.null(x)) y else x } diff --git a/R/version-check.R b/R/version-check.R deleted file mode 100644 index 51495d4..0000000 --- a/R/version-check.R +++ /dev/null @@ -1,30 +0,0 @@ -# Version Checks ---- - -version_above = function(software_version, than) { - utils::compareVersion(software_version, than) == 1L -} - -version_between = function(software_version, lower, greater_strict) { - above = utils::compareVersion(software_version, lower) %in% c(0L, 1L) - below = utils::compareVersion(software_version, greater_strict) %in% c(-1L) - above && below -} - - -is_r_version = function(target_version, compare_major_minor = TRUE) { - - minor_value = if (compare_major_minor) { - # If x.y.z, this retrieves y - strsplit(R.version$minor, ".", fixed = TRUE)[[1]][1] - } else { - # If x.y.z, this retrieves y.z - R.version$minor - } - - # Build the version string of x.y or x.y.z - version_string = paste(R.version$major, minor_value, sep = ".") - - # Check for equality. - return(version_string == target_version) -} - diff --git a/R/xcode-app-ide.R b/R/xcode-app-ide.R deleted file mode 100644 index 3b4beda..0000000 --- a/R/xcode-app-ide.R +++ /dev/null @@ -1,38 +0,0 @@ -#' Detect if the Xcode.app IDE is Installed -#' -#' Checks to see whether Xcode.app Integrated Developer Environment -#' (IDE) was installed and is active under the default location. -#' -#' @details -#' -#' Checks to see if Xcode.app IDE is active by running: -#' -#' ```sh -#' xcode-select -p -#' ``` -#' -#' If it returns: -#' -#' ```sh -#' /Applications/Xcode.app/Contents/Developer -#' ``` -#' -#' We consider the full Xcode.app to be **installed** and **selected** as -#' the command line tools to use. -#' -#' @rdname xcode-app-ide -#' @export -#' @examples -#' # Check if Xcode.app IDE is on the path -#' is_xcode_app_installed() -is_xcode_app_installed = function() { - assert_mac() - - ## Check to see if the Xcode path is set - path_info = xcode_select_path() - - identical(path_info$status, 0L) && - identical(path_info$output, install_directory_xcode_app()) && - dir.exists(path_info$output) - -} diff --git a/R/xcode-cli.R b/R/xcode-cli.R deleted file mode 100644 index cf7ad4e..0000000 --- a/R/xcode-cli.R +++ /dev/null @@ -1,347 +0,0 @@ -#' @include utils.R shell.R installers.R cli-custom.R -NULL - -#' Find, Install, or Uninstall XCode CLI -#' -#' Set of functions that seek to identify whether XCode CLI was installed, -#' allow XCode CLI to be installed, and removing XCode CLI. -#' -#' @section Check if XCode CLI is installed: -#' -#' Checks to see if Xcode CLI returns a viable path to the default Xcode CLI -#' location by checking the output of: -#' -#' ```sh -#' xcode-select -p -#' ``` -#' -#' @rdname xcode-cli -#' @export -#' @examples -#' # Check if Xcode CLI is installed -#' is_xcode_cli_installed() -is_xcode_cli_installed <- function() { - assert_mac() - - path_info <- xcode_select_path() - - identical(path_info$status, 0L) && - identical(path_info$output, install_directory_xcode_cli()) && - dir.exists(path_info$output) -} - -#' @section XCode CLI Installation: -#' -#' The `xcode_cli_install()` function performs a headless or non-interactive -#' installation of the Xcode CLI tools. This installation process requires -#' three steps: -#' -#' 1. Place a temporary file indicating the need to download Xcode CLI -#' 2. Determine the latest version of Xcode CLI by running `softwareupdate` -#' 3. Install the latest version using `softwareupdate` with `sudo`. -#' -#' The alternative approach would be an interactive installation of Xcode CLI -#' by typing into Terminal: -#' -#' ```sh -#' sudo xcode-select --install -#' ``` -#' -#' This command will trigger a pop up window that will walk through the -#' package installation. -#' -#' ### Steps of the Headless CLI Installation -#' -#' The temporary file is created using: -#' -#' ```sh -#' touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress -#' ``` -#' -#' From there, we deduce the latest version of Xcode available to the user -#' through an _R_ sanitized version of the chained _shell_ commands: -#' -#' ```sh -#' product_information=softwareupdate -l | -#' grep '\\*.*Command Line' | -#' tail -n 1 | -#' awk -F"*" '{print $2}' | -#' sed -e 's/^ *//' | -#' sed 's/Label: //g' | -#' tr -d '\n' -#' ``` -#' -#' Then, we trigger the installation process with `sudo` using: -#' -#' ```sh -#' sudo softwareupdate -i "$product_information" --verbose -#' ``` -#' -#' where `$product_information` is obtained from the previous command. -#' -#' Finally, we remove the temporary installation file. -#' -#' ```sh -#' touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress -#' ``` -#' -#' These steps were obtained from Timothy Sutton's -#' [xcode-cli-tools.sh](https://github.com/timsutton/osx-vm-templates/blob/ce8df8a7468faa7c5312444ece1b977c1b2f77a4/scripts/xcode-cli-tools.sh#L8-L14) -#' script and, slightly modernized. -#' -#' @export -#' @rdname xcode-cli -#' @param verbose Display status messages -xcode_cli_install <- function(password = getOption("macrtools.password"), verbose = TRUE){ - assert_mac() - - if (isTRUE(is_xcode_cli_installed())) { - if(verbose) cli_info("Xcode CLI is already installed.") - return(invisible(TRUE)) - } - - if (isTRUE(is_xcode_app_installed())) { - if(verbose) cli_info("Xcode.app IDE is installed. Skipping the commandline installation.") - return(invisible(TRUE)) - } - - temporary_xcli_file <- "/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress" - - # Create a temporary in-progress file - file.create(temporary_xcli_file) - - if (verbose) cli_info("Checking for available Xcode CLI updates...") - - product_information <- - system("softwareupdate -l | - grep '\\*.*Command Line' | - tail -n 1 | - awk -F\"*\" '{print $2}' | - sed -e 's/^ *//' | - sed 's/Label: //g' | - tr -d '\n'", intern = TRUE) - - if (length(product_information) == 0) { - cli_error("Could not find Xcode CLI in software updates. Try installing manually.") - # Remove temporary in-progress file if left in place - if(file.exists(temporary_xcli_file)) { - file.remove(temporary_xcli_file) - } - return(invisible(FALSE)) - } - - if (verbose) { - cli_info(c( - "Installing Xcode CLI version: {.val {product_information}}", - "This process may take 10-15 minutes. Please be patient." - )) - } - - user_password <- password - describe_everything <- isTRUE(verbose) - - cmd <- paste("softwareupdate", "-i", shQuote(product_information), "--verbose") - - xcli_status <- shell_execute(cmd, - sudo = TRUE, password = user_password, verbose = describe_everything) - - # Remove temporary in-progress file if left in place - if(file.exists(temporary_xcli_file)) { - file.remove(temporary_xcli_file) - } - - xcli_clean <- identical(xcli_status, 0L) - - if(isFALSE(xcli_clean)) { - cli_error(c( - "We were not able to install Xcode CLI.", - "Please try to manually install using:", - "{.url https://mac.thecoatlessprofessor.com/macrtools/reference/xcode-cli.html#xcode-cli-installation}" - )) - return(invisible(FALSE)) - } - - if (verbose) { - cli_success("Xcode CLI installed successfully!") - } - - return(invisible(xcli_clean)) -} - - - -#' @section Uninstalling Xcode CLI: -#' -#' The `xcode_cli_uninstall()` attempts to remove _only_ the Xcode CLI tools. -#' -#' Per the [Apple Technical Note TN2339](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_CAN_I_UNINSTALL_THE_COMMAND_LINE_TOOLS_): -#' -#' - Xcode includes all of the command-line tools. If it is installed on your system, remove it to uninstall the command-line tools. -#' - If the `/Library/Developer/CommandLineTools` directory exists on your system, remove it to uninstall the command-line tools -#' -#' Thus, the `xcode_cli_uninstall()` opts to perform the second step **only**. -#' We use an _R_ sanitized _shell_ version of: -#' -#' ```sh -#' sudo rm -rf /Library/Developer/CommandLineTools -#' ``` -#' -#' If the Xcode application is detect, we note that we did not uninstall the -#' Xcode application. Instead, we request the user uninstall the Xcode -#' app using the following steps: -#' -#' 1. Make sure that Xcode is closed. Quit Xcode if needed. -#' 2. Open Finder > Applications, select Xcode and move it to Trash. -#' 3. Empty the trash. -#' 4. In addition, open Terminal and run: -#' -#' ```sh -#' sudo /Developer/Library/uninstall-devtools --mode=all -#' ``` -#' -#' @export -#' @rdname xcode-cli -#' @param password User password to access `sudo`. -xcode_cli_uninstall <- function(password = getOption("macrtools.password"), verbose = TRUE){ - assert_mac() - - if(isFALSE(is_xcode_cli_installed())) { - if(verbose) cli_info("Xcode CLI is not installed.") - return(invisible(TRUE)) - } - - xcli_path <- xcode_cli_path() - - # We should never hit this line of code as the is_xcode_cli_installed() now - # focuses on only CLI. But, we might want to change that. - if(xcli_path == install_directory_xcode_app()) { - cli_warning(c( - "We detected the full Xcode application in use at: {.path {xcode_cli_path()}}", - "Please uninstall it using the App Store or manually." - )) - return(invisible(TRUE)) - } - - if (verbose) { - cli_info("Uninstalling Xcode CLI...") - } - - # Remove the shell execution script - xcli_uninstall_status <- shell_execute("rm -rf /Library/Developer/CommandLineTools", - sudo = TRUE, - password = password) - - xcli_uninstall_clean <- identical(xcli_uninstall_status, 0L) - - if(isFALSE(xcli_uninstall_clean)) { - cli_error(c( - "We were not able to uninstall Xcode CLI.", - "Please try to manually uninstall using:", - "{.url https://mac.thecoatlessprofessor.com/macrtools/reference/xcode-cli.html#uninstalling-xcode-cli}" - )) - return(invisible(FALSE)) - } - - if (verbose) { - cli_success("Xcode CLI uninstalled successfully!") - } - - invisible(xcli_uninstall_clean) -} - -#' @rdname xcode-cli -#' @export -#' @examples -#' # Determine the path location of Xcode CLI -#' xcode_cli_path() -xcode_cli_path <- function() { - inquiry_on_path <- xcode_select_path() - if (identical(inquiry_on_path$status, 0L)) { - inquiry_on_path$output - } else { - "" - } -} - -#' @section Change Xcode CLI Location: -#' If the Xcode Application has been previously installed, the underlying -#' path reported by `xcode-select` may not reflect the Xcode CLI location. -#' The situation can be rectified by using the [xcode_cli_switch()] function, -#' which changes the command line tool directory away from the Xcode application -#' location to the Xcode CLI location. This uses the _default_ Xcode CLI -#' path. -#' -#' ```sh -#' sudo xcode-select --switch /Library/Developer/CommandLineTools -#' ``` -#' -#' If this does not fix the issue, we recommend using the [xcode_cli_reset()] -#' function. -#' @export -#' @rdname xcode-cli -xcode_cli_switch <- function(password = getOption("macrtools.password"), verbose = TRUE) { - # The path matches with the default install directory location. - if (xcode_cli_path() == install_directory_xcode_cli()) { - if (verbose) cli_info("Xcode CLI path is correctly set.") - return(invisible(TRUE)) - } - - # Need to modify the location to the current CLI path - if (verbose) cli_info("Setting the Xcode CLI path...") - - cmd <- paste("xcode-select", "--switch", install_directory_xcode_cli()) - - # Change the directory - xcli_switch_status <- shell_execute(cmd, - sudo = TRUE, - password = password, - verbose = verbose) - - xcli_switch_clean <- identical(xcli_switch_status, 0L) - - if(isFALSE(xcli_switch_clean)) { - cli_error("Failed to switch Xcode CLI path.") - return(invisible(FALSE)) - } - - if (verbose) { - cli_success("Xcode CLI path updated successfully!") - } - - return(invisible(xcli_switch_clean)) -} - -#' @section Reset Xcode CLI: -#' The [xcode_cli_reset()] function uses `xcode-select` to restore -#' the default Xcode CLI settings. -#' -#' We use an _R_ sanitized _shell_ version of: -#' -#' ```sh -#' sudo xcode-select --reset -#' ``` -#' -#' @export -#' @rdname xcode-cli -xcode_cli_reset <- function(password = getOption("macrtools.password"), verbose = TRUE) { - if (verbose) cli_info("Resetting Xcode CLI to default settings...") - - cmd <- paste("xcode-select", "--reset") - - # Change the directory - xcli_reset_status <- shell_execute(cmd, - sudo = TRUE, - password = password, - verbose = verbose) - - xcli_reset_clean <- xcli_reset_status == 0L - - if(isFALSE(xcli_reset_clean)) { - cli_error("Failed to reset Xcode CLI settings.") - return(invisible(FALSE)) - } - - if(verbose) cli_success("Successfully reset Xcode CLI settings!") - - return(invisible(xcli_reset_clean)) -} diff --git a/R/xcode-select.R b/R/xcode-select.R deleted file mode 100644 index 3c55461..0000000 --- a/R/xcode-select.R +++ /dev/null @@ -1,31 +0,0 @@ -#' Interface with `xcode-select` Shell Commands -#' -#' Trigger `xcode-select` commands from within _R_ -#' -#' @param args Flag arguments to pass to `xcode-select` -#' @export -#' @rdname xcode-select -xcode_select = function(args) { - out = sys::exec_internal("xcode-select", args = args, error = FALSE) - - structure( - list( - output = sys::as_text(out$stdout), - error = sys::as_text(out$stderr), - status = out$status - ), - class = c("xcodeselect", "cli") - ) -} - -#' @export -#' @rdname xcode-select -xcode_select_path = function() { - xcode_select("--print-path") -} - -#' @export -#' @rdname xcode-select -xcode_select_version = function() { - xcode_select("--version") -} diff --git a/R/xcode.R b/R/xcode.R new file mode 100644 index 0000000..47ba25e --- /dev/null +++ b/R/xcode.R @@ -0,0 +1,476 @@ +#' Interface with `xcode-select` Shell Commands +#' +#' Trigger `xcode-select` commands from within _R_ +#' +#' @param args Flag arguments to pass to `xcode-select` +#' @export +#' @rdname xcode-select +xcode_select <- function(args) { + out <- sys::exec_internal("xcode-select", args = args, error = FALSE) + + base::structure( + base::list( + output = sys::as_text(out$stdout), + error = sys::as_text(out$stderr), + status = out$status + ), + class = c("xcodeselect", "cli") + ) +} + +#' @export +#' @rdname xcode-select +xcode_select_path <- function() { + xcode_select("--print-path") +} + +#' @export +#' @rdname xcode-select +xcode_select_version <- function() { + xcode_select("--version") +} + +#' Interface with `xcodebuild` Shell Commands +#' +#' Trigger `xcodebuild` commands from within _R_ +#' +#' @param args Flag arguments to pass to `xcodebuild` +#' @export +#' @rdname xcodebuild +xcodebuild <- function(args) { + out <- sys::exec_internal("xcodebuild", args = args, error = FALSE) + + base::structure( + base::list( + output = sys::as_text(out$stdout), + error = sys::as_text(out$stderr), + status = out$status + ), + class = c("xcodebuild", "cli") + ) +} + +#' @export +#' @rdname xcodebuild +xcodebuild_version <- function() { + xcodebuild("-version") +} + +#' Detect if the Xcode.app IDE is Installed +#' +#' Checks to see whether Xcode.app Integrated Developer Environment +#' (IDE) was installed and is active under the default location. +#' +#' @details +#' +#' Checks to see if Xcode.app IDE is active by running: +#' +#' ```sh +#' xcode-select -p +#' ``` +#' +#' If it returns: +#' +#' ```sh +#' /Applications/Xcode.app/Contents/Developer +#' ``` +#' +#' We consider the full Xcode.app to be **installed** and **selected** as +#' the command line tools to use. +#' +#' @rdname xcode-app-ide +#' @export +#' @examples +#' # Check if Xcode.app IDE is on the path +#' is_xcode_app_installed() +is_xcode_app_installed <- function() { + assert_mac() + + # Check to see if the Xcode path is set + path_info <- xcode_select_path() + + base::identical(path_info$status, 0L) && + base::identical(path_info$output, install_directory_xcode_app()) && + base::dir.exists(path_info$output) +} + +#' Find, Install, or Uninstall XCode CLI +#' +#' Set of functions that seek to identify whether XCode CLI was installed, +#' allow XCode CLI to be installed, and removing XCode CLI. +#' +#' @section Check if XCode CLI is installed: +#' +#' Checks to see if Xcode CLI returns a viable path to the default Xcode CLI +#' location by checking the output of: +#' +#' ```sh +#' xcode-select -p +#' ``` +#' +#' @rdname xcode-cli +#' @export +#' @examples +#' # Check if Xcode CLI is installed +#' is_xcode_cli_installed() +is_xcode_cli_installed <- function() { + assert_mac() + + path_info <- xcode_select_path() + + base::identical(path_info$status, 0L) && + base::identical(path_info$output, install_directory_xcode_cli()) && + base::dir.exists(path_info$output) +} + +#' @rdname xcode-cli +#' @export +#' @examples +#' # Determine the path location of Xcode CLI +#' xcode_cli_path() +xcode_cli_path <- function() { + inquiry_on_path <- xcode_select_path() + if (base::identical(inquiry_on_path$status, 0L)) { + inquiry_on_path$output + } else { + "" + } +} + +#' @section XCode CLI Installation: +#' +#' The `xcode_cli_install()` function performs a headless or non-interactive +#' installation of the Xcode CLI tools. This installation process requires +#' three steps: +#' +#' 1. Place a temporary file indicating the need to download Xcode CLI +#' 2. Determine the latest version of Xcode CLI by running `softwareupdate` +#' 3. Install the latest version using `softwareupdate` with `sudo`. +#' +#' The alternative approach would be an interactive installation of Xcode CLI +#' by typing into Terminal: +#' +#' ```sh +#' sudo xcode-select --install +#' ``` +#' +#' This command will trigger a pop up window that will walk through the +#' package installation. +#' +#' ### Steps of the Headless CLI Installation +#' +#' The temporary file is created using: +#' +#' ```sh +#' touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress +#' ``` +#' +#' From there, we deduce the latest version of Xcode available to the user +#' through an _R_ sanitized version of the chained _shell_ commands: +#' +#' ```sh +#' product_information=softwareupdate -l | +#' grep '\\*.*Command Line' | +#' tail -n 1 | +#' awk -F"*" '{print $2}' | +#' sed -e 's/^ *//' | +#' sed 's/Label: //g' | +#' tr -d '\n' +#' ``` +#' +#' Then, we trigger the installation process with `sudo` using: +#' +#' ```sh +#' sudo softwareupdate -i "$product_information" --verbose +#' ``` +#' +#' where `$product_information` is obtained from the previous command. +#' +#' Finally, we remove the temporary installation file. +#' +#' ```sh +#' touch /tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress +#' ``` +#' +#' These steps were obtained from Timothy Sutton's +#' [xcode-cli-tools.sh](https://github.com/timsutton/osx-vm-templates/blob/ce8df8a7468faa7c5312444ece1b977c1b2f77a4/scripts/xcode-cli-tools.sh#L8-L14) +#' script and, slightly modernized. +#' +#' @export +#' @rdname xcode-cli +#' @param verbose Display status messages +#' @param password User password to access `sudo`. +xcode_cli_install <- function(password = base::getOption("macrtools.password"), verbose = TRUE){ + assert_mac() + + if (base::isTRUE(is_xcode_cli_installed())) { + if(verbose) { + cli::cli_alert_info("{.pkg macrtools}: Xcode CLI is already installed.") + cli::cli_text("") # Add spacing + } + return(base::invisible(TRUE)) + } + + if (base::isTRUE(is_xcode_app_installed())) { + if(verbose) { + cli::cli_alert_info("{.pkg macrtools}: Xcode.app IDE is installed.") + cli::cli_text("Skipping the commandline installation.") + cli::cli_text("") # Add spacing + } + return(base::invisible(TRUE)) + } + + temporary_xcli_file <- "/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress" + + # Create a temporary in-progress file + base::file.create(temporary_xcli_file) + + if (verbose) { + cli::cli_alert_info("{.pkg macrtools}: Checking for available Xcode CLI updates.") + cli::cli_text("") # Add spacing + } + + product_information <- + base::system("softwareupdate -l | + grep '\\*.*Command Line' | + tail -n 1 | + awk -F\"*\" '{print $2}' | + sed -e 's/^ *//' | + sed 's/Label: //g' | + tr -d '\n'", intern = TRUE) + + if (base::length(product_information) == 0) { + cli::cli_abort(c( + "{.pkg macrtools}: Could not find Xcode CLI in software updates.", + "i" = "Try installing manually with 'xcode-select --install' in Terminal." + )) + + # Remove temporary in-progress file if left in place + if(base::file.exists(temporary_xcli_file)) { + base::file.remove(temporary_xcli_file) + } + return(base::invisible(FALSE)) + } + + if (verbose) { + cli::cli_alert_info("{.pkg macrtools}: Installing Xcode CLI.") + cli::cli_bullets(c( + "Version: {.val {product_information}}", + "This process may take 10-15 minutes. Please be patient." + )) + cli::cli_text("") # Add spacing + } + + cmd <- base::paste("softwareupdate", "-i", base::shQuote(product_information), "--verbose") + + xcli_status <- shell_execute(cmd, + sudo = TRUE, password = password, verbose = verbose) + + # Remove temporary in-progress file if left in place + if(base::file.exists(temporary_xcli_file)) { + base::file.remove(temporary_xcli_file) + } + + xcli_clean <- base::identical(xcli_status, 0L) + + if(base::isFALSE(xcli_clean)) { + cli::cli_abort(c( + "{.pkg macrtools}: We were not able to install Xcode CLI.", + "i" = "Please try to manually install using: https://mac.thecoatlessprofessor.com/macrtools/reference/xcode-cli.html#xcode-cli-installation" + )) + return(base::invisible(FALSE)) + } + + if (verbose) { + cli::cli_alert_success("{.pkg macrtools}: Xcode CLI installed successfully!") + cli::cli_text("") # Add spacing + } + + return(base::invisible(xcli_clean)) +} + + +#' @section Uninstalling Xcode CLI: +#' +#' The `xcode_cli_uninstall()` attempts to remove _only_ the Xcode CLI tools. +#' +#' Per the [Apple Technical Note TN2339](https://developer.apple.com/library/archive/technotes/tn2339/_index.html#//apple_ref/doc/uid/DTS40014588-CH1-HOW_CAN_I_UNINSTALL_THE_COMMAND_LINE_TOOLS_): +#' +#' - Xcode includes all of the command-line tools. If it is installed on your system, remove it to uninstall the command-line tools. +#' - If the `/Library/Developer/CommandLineTools` directory exists on your system, remove it to uninstall the command-line tools +#' +#' Thus, the `xcode_cli_uninstall()` opts to perform the second step **only**. +#' We use an _R_ sanitized _shell_ version of: +#' +#' ```sh +#' sudo rm -rf /Library/Developer/CommandLineTools +#' ``` +#' +#' If the Xcode application is detect, we note that we did not uninstall the +#' Xcode application. Instead, we request the user uninstall the Xcode +#' app using the following steps: +#' +#' 1. Make sure that Xcode is closed. Quit Xcode if needed. +#' 2. Open Finder > Applications, select Xcode and move it to Trash. +#' 3. Empty the trash. +#' 4. In addition, open Terminal and run: +#' +#' ```sh +#' sudo /Developer/Library/uninstall-devtools --mode=all +#' ``` +#' +#' @export +#' @rdname xcode-cli +xcode_cli_uninstall <- function(password = base::getOption("macrtools.password"), verbose = TRUE){ + assert_mac() + + if(base::isFALSE(is_xcode_cli_installed())) { + if(verbose) { + cli::cli_alert_info("{.pkg macrtools}: Xcode CLI is not installed.") + cli::cli_text("") # Add spacing + } + return(base::invisible(TRUE)) + } + + xcli_path <- xcode_cli_path() + + # We should never hit this line of code as the is_xcode_cli_installed() now + # focuses on only CLI. But, we might want to change that. + if(xcli_path == install_directory_xcode_app()) { + cli::cli_alert_warning("{.pkg macrtools}: Full Xcode application detected.") + cli::cli_bullets(c( + "Path: {.path {xcode_cli_path()}}", + "Please uninstall it using the App Store or manually." + )) + cli::cli_text("") # Add spacing + return(base::invisible(TRUE)) + } + + if (verbose) { + cli::cli_alert_info("{.pkg macrtools}: Uninstalling Xcode CLI.") + cli::cli_text("") # Add spacing + } + + # Remove the shell execution script + xcli_uninstall_status <- shell_execute("rm -rf /Library/Developer/CommandLineTools", + sudo = TRUE, + password = password) + + xcli_uninstall_clean <- base::identical(xcli_uninstall_status, 0L) + + if(base::isFALSE(xcli_uninstall_clean)) { + cli::cli_abort(c( + "{.pkg macrtools}: We were not able to uninstall Xcode CLI.", + "i" = "Please try to manually uninstall using: https://mac.thecoatlessprofessor.com/macrtools/reference/xcode-cli.html#uninstalling-xcode-cli" + )) + return(base::invisible(FALSE)) + } + + if (verbose) { + cli::cli_alert_success("{.pkg macrtools}: Xcode CLI uninstalled successfully!") + cli::cli_text("") # Add spacing + } + + base::invisible(xcli_uninstall_clean) +} + + +#' @section Change Xcode CLI Location: +#' If the Xcode Application has been previously installed, the underlying +#' path reported by `xcode-select` may not reflect the Xcode CLI location. +#' The situation can be rectified by using the [xcode_cli_switch()] function, +#' which changes the command line tool directory away from the Xcode application +#' location to the Xcode CLI location. This uses the _default_ Xcode CLI +#' path. +#' +#' ```sh +#' sudo xcode-select --switch /Library/Developer/CommandLineTools +#' ``` +#' +#' If this does not fix the issue, we recommend using the [xcode_cli_reset()] +#' function. +#' @export +#' @rdname xcode-cli +xcode_cli_switch <- function(password = base::getOption("macrtools.password"), verbose = TRUE) { + # The path matches with the default install directory location. + if (xcode_cli_path() == install_directory_xcode_cli()) { + if (verbose) { + cli::cli_alert_info("{.pkg macrtools}: Xcode CLI path is correctly set.") + cli::cli_text("") # Add spacing + } + return(base::invisible(TRUE)) + } + + # Need to modify the location to the current CLI path + if (verbose) { + cli::cli_alert_info("{.pkg macrtools}: Setting the Xcode CLI path.") + cli::cli_bullets(c( + "Current path: {.path {xcode_cli_path()}}", + "Target path: {.path {install_directory_xcode_cli()}}" + )) + cli::cli_text("") # Add spacing + } + + cmd <- base::paste("xcode-select", "--switch", install_directory_xcode_cli()) + + # Change the directory + xcli_switch_status <- shell_execute(cmd, + sudo = TRUE, + password = password, + verbose = verbose) + + xcli_switch_clean <- base::identical(xcli_switch_status, 0L) + + if(base::isFALSE(xcli_switch_clean)) { + cli::cli_abort("{.pkg macrtools}: Failed to switch Xcode CLI path.") + return(base::invisible(FALSE)) + } + + if (verbose) { + cli::cli_alert_success("{.pkg macrtools}: Xcode CLI path updated successfully!") + cli::cli_text("") # Add spacing + } + + return(base::invisible(xcli_switch_clean)) +} + +#' @section Reset Xcode CLI: +#' The [xcode_cli_reset()] function uses `xcode-select` to restore +#' the default Xcode CLI settings. +#' +#' We use an _R_ sanitized _shell_ version of: +#' +#' ```sh +#' sudo xcode-select --reset +#' ``` +#' +#' @export +#' @rdname xcode-cli +xcode_cli_reset <- function(password = base::getOption("macrtools.password"), verbose = TRUE) { + if (verbose) { + cli::cli_alert_info("{.pkg macrtools}: Resetting Xcode CLI to default settings.") + cli::cli_text("") # Add spacing + } + + cmd <- base::paste("xcode-select", "--reset") + + # Change the directory + xcli_reset_status <- shell_execute(cmd, + sudo = TRUE, + password = password, + verbose = verbose) + + xcli_reset_clean <- xcli_reset_status == 0L + + if(base::isFALSE(xcli_reset_clean)) { + cli::cli_abort("{.pkg macrtools}: Failed to reset Xcode CLI settings.") + return(base::invisible(FALSE)) + } + + if(verbose) { + cli::cli_alert_success("{.pkg macrtools}: Successfully reset Xcode CLI settings!") + cli::cli_text("") # Add spacing + } + + return(base::invisible(xcli_reset_clean)) +} diff --git a/R/xcodebuild.R b/R/xcodebuild.R deleted file mode 100644 index 06df69c..0000000 --- a/R/xcodebuild.R +++ /dev/null @@ -1,25 +0,0 @@ -#' Interface with `xcodebuild` Shell Commands -#' -#' Trigger `xcodebuild` commands from within _R_ -#' -#' @param args Flag arguments to pass to `xcodebuild` -#' @export -#' @rdname xcodebuild -xcodebuild = function(args) { - out = sys::exec_internal("xcodebuild", args = args, error = FALSE) - - structure( - list( - output = sys::as_text(out$stdout), - error = sys::as_text(out$stderr), - status = out$status - ), - class = c("xcodebuild", "cli") - ) -} - -#' @export -#' @rdname xcodebuild -xcodebuild_version = function() { - xcodebuild("-version") -} diff --git a/R/zzz.R b/R/zzz.R index 63592dc..02bd6f8 100644 --- a/R/zzz.R +++ b/R/zzz.R @@ -1,14 +1,13 @@ -#' @include cli-custom.R - .onAttach <- function(libname, pkgname) { # Only run on interactive mode if (!base::interactive()) return() # Check if it's actually macOS if (!is_macos()) { + os_info <- base::tolower(base::Sys.info()[['sysname']]) base::packageStartupMessage(cli::format_inline( "{.pkg macrtools}: {.emph Warning: This package is designed for macOS only.}", - "\n{.pkg macrtools}: {.emph Current OS: {.val {tolower(base::Sys.info()[['sysname']])}}}", + "\n{.pkg macrtools}: {.emph Current OS: {.val {os_info}}}", "\n{.pkg macrtools}: {.emph See https://mac.thecoatlessprofessor.com/macrtools/ for more information.}" )) return() @@ -30,8 +29,9 @@ } else { # Show welcome message on supported system if (base::getOption("macrtools.show_welcome", TRUE)) { + mac_version <- shell_mac_version() base::packageStartupMessage(cli::format_inline( - "{.pkg macrtools}: Ready to set up R development tools on macOS {.val {shell_mac_version()}}.", + "{.pkg macrtools}: Ready to set up R development tools on macOS {.val {mac_version}}.", "\n{.pkg macrtools}: Run {.code macrtools::macos_rtools_install()} to begin installation.", "\n{.pkg macrtools}: For help, see: {.url https://mac.thecoatlessprofessor.com/macrtools/}" )) diff --git a/man/caller_env.Rd b/man/caller_env.Rd new file mode 100644 index 0000000..b45bf64 --- /dev/null +++ b/man/caller_env.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{caller_env} +\alias{caller_env} +\title{Get Caller Environment} +\usage{ +caller_env(n = 1) +} +\arguments{ +\item{n}{Number of frames to go back} +} +\value{ +Caller environment +} +\description{ +Get Caller Environment +} +\keyword{internal} diff --git a/man/cli-utils.Rd b/man/cli-utils.Rd deleted file mode 100644 index a75d741..0000000 --- a/man/cli-utils.Rd +++ /dev/null @@ -1,9 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli-utils} -\alias{cli-utils} -\title{CLI Utility Functions} -\description{ -Utility functions for consistent CLI messaging throughout the package. -} -\keyword{internal} diff --git a/man/cli_danger.Rd b/man/cli_danger.Rd deleted file mode 100644 index 00eccf0..0000000 --- a/man/cli_danger.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_danger} -\alias{cli_danger} -\title{Display a danger/important message} -\usage{ -cli_danger(..., .envir = parent.frame()) -} -\arguments{ -\item{...}{Message parts, passed to cli::format_inline} - -\item{.envir}{Environment to evaluate in} -} -\description{ -Display a danger/important message -} -\keyword{internal} diff --git a/man/cli_error.Rd b/man/cli_error.Rd deleted file mode 100644 index dd72323..0000000 --- a/man/cli_error.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_error} -\alias{cli_error} -\title{Throw an error with a formatted message} -\usage{ -cli_error(..., .envir = parent.frame(), call = caller_env(), advice = NULL) -} -\arguments{ -\item{...}{Message parts, passed to cli::format_inline} - -\item{.envir}{Environment to evaluate in} - -\item{call}{The calling environment} - -\item{advice}{Optional advice to provide to the user} -} -\description{ -Throw an error with a formatted message -} -\keyword{internal} diff --git a/man/cli_info.Rd b/man/cli_info.Rd deleted file mode 100644 index ec2caae..0000000 --- a/man/cli_info.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_info} -\alias{cli_info} -\title{Display an info message} -\usage{ -cli_info(..., .envir = parent.frame()) -} -\arguments{ -\item{...}{Message parts, passed to cli::format_inline} - -\item{.envir}{Environment to evaluate in} -} -\description{ -Display an info message -} -\keyword{internal} diff --git a/man/cli_process_done.Rd b/man/cli_process_done.Rd deleted file mode 100644 index 3b8fad5..0000000 --- a/man/cli_process_done.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_process_done} -\alias{cli_process_done} -\title{Complete a process} -\usage{ -cli_process_done(id, msg = NULL) -} -\arguments{ -\item{id}{The progress bar ID} - -\item{msg}{Optional completion message} -} -\description{ -Complete a process -} -\keyword{internal} diff --git a/man/cli_process_start.Rd b/man/cli_process_start.Rd deleted file mode 100644 index 4da67b5..0000000 --- a/man/cli_process_start.Rd +++ /dev/null @@ -1,24 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_process_start} -\alias{cli_process_start} -\title{Display the start of a process} -\usage{ -cli_process_start(msg, .envir = parent.frame(), total = 100, clear = FALSE) -} -\arguments{ -\item{msg}{The process description} - -\item{.envir}{Environment to evaluate in} - -\item{total}{The total number of steps (default: 100)} - -\item{clear}{Whether to clear the progress bar when done (default: FALSE)} -} -\value{ -The ID of the progress bar -} -\description{ -Display the start of a process -} -\keyword{internal} diff --git a/man/cli_process_update.Rd b/man/cli_process_update.Rd deleted file mode 100644 index 2b42898..0000000 --- a/man/cli_process_update.Rd +++ /dev/null @@ -1,19 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_process_update} -\alias{cli_process_update} -\title{Update a process progress} -\usage{ -cli_process_update(id, ratio, status_msg = NULL) -} -\arguments{ -\item{id}{The progress bar ID} - -\item{ratio}{The progress ratio (0-1) or number of steps completed} - -\item{status_msg}{Optional status message to display} -} -\description{ -Update a process progress -} -\keyword{internal} diff --git a/man/cli_success.Rd b/man/cli_success.Rd deleted file mode 100644 index a489ab0..0000000 --- a/man/cli_success.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_success} -\alias{cli_success} -\title{Display a success message} -\usage{ -cli_success(..., .envir = parent.frame()) -} -\arguments{ -\item{...}{Message parts, passed to cli::format_inline} - -\item{.envir}{Environment to evaluate in} -} -\description{ -Display a success message -} -\keyword{internal} diff --git a/man/cli_system_update.Rd b/man/cli_system_update.Rd deleted file mode 100644 index 37cbcfb..0000000 --- a/man/cli_system_update.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_system_update} -\alias{cli_system_update} -\title{Display a detailed system update} -\usage{ -cli_system_update(title, items) -} -\arguments{ -\item{title}{Title of the update section} - -\item{items}{Vector of items to display as a bulleted list} -} -\description{ -Display a detailed system update -} -\keyword{internal} diff --git a/man/cli_waiting.Rd b/man/cli_waiting.Rd deleted file mode 100644 index e3dc14c..0000000 --- a/man/cli_waiting.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_waiting} -\alias{cli_waiting} -\title{Display a waiting message} -\usage{ -cli_waiting(..., .envir = parent.frame()) -} -\arguments{ -\item{...}{Message parts, passed to cli::format_inline} - -\item{.envir}{Environment to evaluate in} -} -\description{ -Display a waiting message -} -\keyword{internal} diff --git a/man/cli_warning.Rd b/man/cli_warning.Rd deleted file mode 100644 index 38fa5c6..0000000 --- a/man/cli_warning.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{cli_warning} -\alias{cli_warning} -\title{Display a warning message} -\usage{ -cli_warning(..., .envir = parent.frame()) -} -\arguments{ -\item{...}{Message parts, passed to cli::format_inline} - -\item{.envir}{Environment to evaluate in} -} -\description{ -Display a warning message -} -\keyword{internal} diff --git a/man/force_password.Rd b/man/force_password.Rd new file mode 100644 index 0000000..2c72c43 --- /dev/null +++ b/man/force_password.Rd @@ -0,0 +1,18 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{force_password} +\alias{force_password} +\title{Force Password Entry if Not Provided} +\usage{ +force_password(supplied_password) +} +\arguments{ +\item{supplied_password}{Password provided by user (may be NULL)} +} +\value{ +Password to use for operations +} +\description{ +Force Password Entry if Not Provided +} +\keyword{internal} diff --git a/man/format_cmd.Rd b/man/format_cmd.Rd deleted file mode 100644 index 127becb..0000000 --- a/man/format_cmd.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{format_cmd} -\alias{format_cmd} -\title{Format a command for display} -\usage{ -format_cmd(cmd) -} -\arguments{ -\item{cmd}{The command to format} -} -\value{ -A formatted string with code styling -} -\description{ -Format a command for display -} -\keyword{internal} diff --git a/man/format_msg.Rd b/man/format_msg.Rd deleted file mode 100644 index 0293d07..0000000 --- a/man/format_msg.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{format_msg} -\alias{format_msg} -\title{Format a message with a package prefix} -\usage{ -format_msg(msg, .envir = parent.frame()) -} -\arguments{ -\item{msg}{The message to format} - -\item{.envir}{Environment to evaluate in} -} -\value{ -A formatted string with package prefix -} -\description{ -Format a message with a package prefix -} -\keyword{internal} diff --git a/man/format_path.Rd b/man/format_path.Rd deleted file mode 100644 index abf086d..0000000 --- a/man/format_path.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/cli-custom.R -\name{format_path} -\alias{format_path} -\title{Format a path for display} -\usage{ -format_path(path) -} -\arguments{ -\item{path}{The file path to format} -} -\value{ -A formatted string with path styling -} -\description{ -Format a path for display -} -\keyword{internal} diff --git a/man/gfortran.Rd b/man/gfortran.Rd index 5ef0bb5..6c6b640 100644 --- a/man/gfortran.Rd +++ b/man/gfortran.Rd @@ -12,11 +12,20 @@ is_gfortran_installed() gfortran_version() -gfortran_install(password = getOption("macrtools.password"), verbose = TRUE) - -gfortran_uninstall(password = getOption("macrtools.password"), verbose = TRUE) - -gfortran_update(password = getOption("macrtools.password"), verbose = TRUE) +gfortran_install( + password = base::getOption("macrtools.password"), + verbose = TRUE +) + +gfortran_uninstall( + password = base::getOption("macrtools.password"), + verbose = TRUE +) + +gfortran_update( + password = base::getOption("macrtools.password"), + verbose = TRUE +) } \arguments{ \item{password}{Password for user account to install software. Default is diff --git a/man/is_aarch64.Rd b/man/is_aarch64.Rd new file mode 100644 index 0000000..215730c --- /dev/null +++ b/man/is_aarch64.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_aarch64} +\alias{is_aarch64} +\title{Check if System is aarch64} +\usage{ +is_aarch64() +} +\value{ +TRUE if system is Apple Silicon (M-series) Mac, FALSE otherwise +} +\description{ +Check if System is aarch64 +} +\keyword{internal} diff --git a/man/is_macos.Rd b/man/is_macos.Rd new file mode 100644 index 0000000..26b5629 --- /dev/null +++ b/man/is_macos.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos} +\alias{is_macos} +\title{Check if System is macOS} +\usage{ +is_macos() +} +\value{ +TRUE if system is macOS, FALSE otherwise +} +\description{ +Check if System is macOS +} +\keyword{internal} diff --git a/man/is_macos_big_sur.Rd b/man/is_macos_big_sur.Rd new file mode 100644 index 0000000..939d638 --- /dev/null +++ b/man/is_macos_big_sur.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos_big_sur} +\alias{is_macos_big_sur} +\title{Check if macOS Big Sur} +\usage{ +is_macos_big_sur() +} +\value{ +TRUE if system is macOS Big Sur, FALSE otherwise +} +\description{ +Check if macOS Big Sur +} +\keyword{internal} diff --git a/man/is_macos_catalina.Rd b/man/is_macos_catalina.Rd new file mode 100644 index 0000000..e01e4ab --- /dev/null +++ b/man/is_macos_catalina.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos_catalina} +\alias{is_macos_catalina} +\title{Check if macOS Catalina} +\usage{ +is_macos_catalina() +} +\value{ +TRUE if system is macOS Catalina, FALSE otherwise +} +\description{ +Check if macOS Catalina +} +\keyword{internal} diff --git a/man/is_macos_high_sierra.Rd b/man/is_macos_high_sierra.Rd new file mode 100644 index 0000000..0ab716d --- /dev/null +++ b/man/is_macos_high_sierra.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos_high_sierra} +\alias{is_macos_high_sierra} +\title{Check if macOS High Sierra} +\usage{ +is_macos_high_sierra() +} +\value{ +TRUE if system is macOS High Sierra, FALSE otherwise +} +\description{ +Check if macOS High Sierra +} +\keyword{internal} diff --git a/man/is_macos_mojave.Rd b/man/is_macos_mojave.Rd new file mode 100644 index 0000000..61fafaa --- /dev/null +++ b/man/is_macos_mojave.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos_mojave} +\alias{is_macos_mojave} +\title{Check if macOS Mojave} +\usage{ +is_macos_mojave() +} +\value{ +TRUE if system is macOS Mojave, FALSE otherwise +} +\description{ +Check if macOS Mojave +} +\keyword{internal} diff --git a/man/is_macos_monterey.Rd b/man/is_macos_monterey.Rd new file mode 100644 index 0000000..2006ed3 --- /dev/null +++ b/man/is_macos_monterey.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos_monterey} +\alias{is_macos_monterey} +\title{Check if macOS Monterey} +\usage{ +is_macos_monterey() +} +\value{ +TRUE if system is macOS Monterey, FALSE otherwise +} +\description{ +Check if macOS Monterey +} +\keyword{internal} diff --git a/man/is_macos_r_supported.Rd b/man/is_macos_r_supported.Rd new file mode 100644 index 0000000..cb8c535 --- /dev/null +++ b/man/is_macos_r_supported.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos_r_supported} +\alias{is_macos_r_supported} +\title{Check if macOS Version is Supported for R} +\usage{ +is_macos_r_supported() +} +\value{ +TRUE if macOS version is supported, FALSE otherwise +} +\description{ +Check if macOS Version is Supported for R +} +\keyword{internal} diff --git a/man/is_macos_sequoia.Rd b/man/is_macos_sequoia.Rd new file mode 100644 index 0000000..e5dc009 --- /dev/null +++ b/man/is_macos_sequoia.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos_sequoia} +\alias{is_macos_sequoia} +\title{Check if macOS Sequoia} +\usage{ +is_macos_sequoia() +} +\value{ +TRUE if system is macOS Sequoia, FALSE otherwise +} +\description{ +Check if macOS Sequoia +} +\keyword{internal} diff --git a/man/is_macos_sonoma.Rd b/man/is_macos_sonoma.Rd new file mode 100644 index 0000000..43ad91b --- /dev/null +++ b/man/is_macos_sonoma.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos_sonoma} +\alias{is_macos_sonoma} +\title{Check if macOS Sonoma} +\usage{ +is_macos_sonoma() +} +\value{ +TRUE if system is macOS Sonoma, FALSE otherwise +} +\description{ +Check if macOS Sonoma +} +\keyword{internal} diff --git a/man/is_macos_ventura.Rd b/man/is_macos_ventura.Rd new file mode 100644 index 0000000..2e695aa --- /dev/null +++ b/man/is_macos_ventura.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_macos_ventura} +\alias{is_macos_ventura} +\title{Check if macOS Ventura} +\usage{ +is_macos_ventura() +} +\value{ +TRUE if system is macOS Ventura, FALSE otherwise +} +\description{ +Check if macOS Ventura +} +\keyword{internal} diff --git a/man/is_r_version.Rd b/man/is_r_version.Rd new file mode 100644 index 0000000..db59a6e --- /dev/null +++ b/man/is_r_version.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_r_version} +\alias{is_r_version} +\title{Check R Version} +\usage{ +is_r_version(target_version, compare_major_minor = TRUE) +} +\arguments{ +\item{target_version}{Target R version to check against (e.g., "4.0")} + +\item{compare_major_minor}{Whether to compare only major.minor (TRUE) or major.minor.patch (FALSE)} +} +\value{ +TRUE if R version matches target_version, FALSE otherwise +} +\description{ +Check R Version +} +\keyword{internal} diff --git a/man/is_x86_64.Rd b/man/is_x86_64.Rd new file mode 100644 index 0000000..a399770 --- /dev/null +++ b/man/is_x86_64.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{is_x86_64} +\alias{is_x86_64} +\title{Check if System is x86_64} +\usage{ +is_x86_64() +} +\value{ +TRUE if system is Intel-based Mac, FALSE otherwise +} +\description{ +Check if System is x86_64 +} +\keyword{internal} diff --git a/man/macos-rtools.Rd b/man/macos-rtools.Rd index 5ee4c39..5aae017 100644 --- a/man/macos-rtools.Rd +++ b/man/macos-rtools.Rd @@ -6,12 +6,12 @@ \title{Install and Uninstall the macOS R Toolchain} \usage{ macos_rtools_install( - password = getOption("macrtools.password"), + password = base::getOption("macrtools.password"), verbose = TRUE ) macos_rtools_uninstall( - password = getOption("macrtools.password"), + password = base::getOption("macrtools.password"), verbose = TRUE ) } diff --git a/man/null_coalesce.Rd b/man/null_coalesce.Rd new file mode 100644 index 0000000..5506d47 --- /dev/null +++ b/man/null_coalesce.Rd @@ -0,0 +1,21 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{null_coalesce} +\alias{null_coalesce} +\alias{\%||\%} +\title{Null Coalesce Operator} +\usage{ +x \%||\% y +} +\arguments{ +\item{x}{First value (may be NULL)} + +\item{y}{Default value if x is NULL} +} +\value{ +x if not NULL, otherwise y +} +\description{ +Null Coalesce Operator +} +\keyword{internal} diff --git a/man/recipes_binary_install.Rd b/man/recipes_binary_install.Rd index 7d7768b..4cd1545 100644 --- a/man/recipes_binary_install.Rd +++ b/man/recipes_binary_install.Rd @@ -7,12 +7,12 @@ recipes_binary_install( pkgs, url = "https://mac.R-project.org/bin", - os = tolower(paste0(system("uname -s", intern = TRUE), gsub("\\\\..*", "", - system("uname -r", intern = TRUE)))), - arch = system("uname -m", intern = TRUE), + os = base::tolower(base::paste0(base::system("uname -s", intern = TRUE), + base::gsub("\\\\..*", "", base::system("uname -r", intern = TRUE)))), + arch = base::system("uname -m", intern = TRUE), os.arch = "auto", dependencies = TRUE, - action = c("install", "list"), + action = c("install", "list", "download"), sudo = TRUE, password = NULL, verbose = TRUE @@ -33,15 +33,16 @@ Default \code{"auto"}.} \item{dependencies}{Install build dependencies (\code{TRUE}) or only the requested packages (\code{FALSE}). Default \code{TRUE}.} -\item{action}{Determine if the binary should be downloaded and installed (\code{"install"}) -or displayed (\code{"list"}). Default \code{"install"} to download and install the binaries.} +\item{action}{Determine if the binary should be downloaded and installed (\code{"install"}), +displayed (\code{"list"}), or downloaded but not installed (\code{"download"}). +Default \code{"install"} to download and install the binaries.} \item{sudo}{Attempt to install the binaries using \code{sudo} permissions. Default \code{TRUE}.} \item{password}{User password to switch into the \code{sudo} user. Default \code{NULL}.} -\item{verbose}{Describe the steps being taken. Default \code{TRUE}} +\item{verbose}{Describe the steps being taken. Default \code{TRUE}.} } \description{ Convenience function that seeks to install pre-built binary libraries @@ -58,6 +59,12 @@ repository and the install path are either:\tabular{lll}{ \href{https://mac.r-project.org/bin/darwin20/arm64}{darwin20/arm64} \tab /opt/R/arm64 \tab macOS 11, Apple M1 (arm64) \cr } } +\section{Differences}{ + +The official implementation uses \code{quiet} as a parameter to suppress output +instead of \code{verbose}. +} + \examples{ # Perform a dry-run to see the required development packages. recipes_binary_install("r-base-dev", action = "list") diff --git a/man/shell_mac_version.Rd b/man/shell_mac_version.Rd new file mode 100644 index 0000000..a04852a --- /dev/null +++ b/man/shell_mac_version.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{shell_mac_version} +\alias{shell_mac_version} +\title{Get macOS Version} +\usage{ +shell_mac_version() +} +\value{ +The macOS version string +} +\description{ +Get macOS Version +} +\keyword{internal} diff --git a/man/system_arch.Rd b/man/system_arch.Rd new file mode 100644 index 0000000..27717ba --- /dev/null +++ b/man/system_arch.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{system_arch} +\alias{system_arch} +\title{System Architecture Detection} +\usage{ +system_arch() +} +\value{ +The system architecture identifier +} +\description{ +System Architecture Detection +} +\keyword{internal} diff --git a/man/system_os.Rd b/man/system_os.Rd new file mode 100644 index 0000000..b107faf --- /dev/null +++ b/man/system_os.Rd @@ -0,0 +1,15 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{system_os} +\alias{system_os} +\title{System OS Detection} +\usage{ +system_os() +} +\value{ +The name of the operating system in lowercase +} +\description{ +System OS Detection +} +\keyword{internal} diff --git a/man/verify_status.Rd b/man/verify_status.Rd new file mode 100644 index 0000000..e0482d7 --- /dev/null +++ b/man/verify_status.Rd @@ -0,0 +1,24 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/utils.R +\name{verify_status} +\alias{verify_status} +\title{Verify Status of Operation} +\usage{ +verify_status(status, program, url, type = c("uninstall", "install")) +} +\arguments{ +\item{status}{Status code from operation} + +\item{program}{Name of the program being installed or uninstalled} + +\item{url}{Optional URL for manual instructions} + +\item{type}{Type of operation ("uninstall" or "install")} +} +\value{ +TRUE if status is successful, FALSE otherwise (invisibly) +} +\description{ +Verify Status of Operation +} +\keyword{internal} diff --git a/man/version_above.Rd b/man/version_above.Rd new file mode 100644 index 0000000..4121ba2 --- /dev/null +++ b/man/version_above.Rd @@ -0,0 +1,20 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{version_above} +\alias{version_above} +\title{Check if Version is Above Threshold} +\usage{ +version_above(software_version, than) +} +\arguments{ +\item{software_version}{Version string to check} + +\item{than}{Threshold version to compare against} +} +\value{ +TRUE if software_version is above than, FALSE otherwise +} +\description{ +Check if Version is Above Threshold +} +\keyword{internal} diff --git a/man/version_between.Rd b/man/version_between.Rd new file mode 100644 index 0000000..2709407 --- /dev/null +++ b/man/version_between.Rd @@ -0,0 +1,22 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/system.R +\name{version_between} +\alias{version_between} +\title{Check if Version is Between Bounds} +\usage{ +version_between(software_version, lower, greater_strict) +} +\arguments{ +\item{software_version}{Version string to check} + +\item{lower}{Lower bound for version check (inclusive)} + +\item{greater_strict}{Upper bound for version check (exclusive)} +} +\value{ +TRUE if software_version is between bounds, FALSE otherwise +} +\description{ +Check if Version is Between Bounds +} +\keyword{internal} diff --git a/man/xcode-app-ide.Rd b/man/xcode-app-ide.Rd index f6c9354..eb38f97 100644 --- a/man/xcode-app-ide.Rd +++ b/man/xcode-app-ide.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xcode-app-ide.R +% Please edit documentation in R/xcode.R \name{is_xcode_app_installed} \alias{is_xcode_app_installed} \title{Detect if the Xcode.app IDE is Installed} diff --git a/man/xcode-cli.Rd b/man/xcode-cli.Rd index b0c1c9b..3d99e11 100644 --- a/man/xcode-cli.Rd +++ b/man/xcode-cli.Rd @@ -1,25 +1,37 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xcode-cli.R +% Please edit documentation in R/xcode.R \name{is_xcode_cli_installed} \alias{is_xcode_cli_installed} +\alias{xcode_cli_path} \alias{xcode_cli_install} \alias{xcode_cli_uninstall} -\alias{xcode_cli_path} \alias{xcode_cli_switch} \alias{xcode_cli_reset} \title{Find, Install, or Uninstall XCode CLI} \usage{ is_xcode_cli_installed() -xcode_cli_install(password = getOption("macrtools.password"), verbose = TRUE) - -xcode_cli_uninstall(password = getOption("macrtools.password"), verbose = TRUE) - xcode_cli_path() -xcode_cli_switch(password = getOption("macrtools.password"), verbose = TRUE) - -xcode_cli_reset(password = getOption("macrtools.password"), verbose = TRUE) +xcode_cli_install( + password = base::getOption("macrtools.password"), + verbose = TRUE +) + +xcode_cli_uninstall( + password = base::getOption("macrtools.password"), + verbose = TRUE +) + +xcode_cli_switch( + password = base::getOption("macrtools.password"), + verbose = TRUE +) + +xcode_cli_reset( + password = base::getOption("macrtools.password"), + verbose = TRUE +) } \arguments{ \item{password}{User password to access \code{sudo}.} diff --git a/man/xcode-select.Rd b/man/xcode-select.Rd index 34b472c..4204324 100644 --- a/man/xcode-select.Rd +++ b/man/xcode-select.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xcode-select.R +% Please edit documentation in R/xcode.R \name{xcode_select} \alias{xcode_select} \alias{xcode_select_path} diff --git a/man/xcodebuild.Rd b/man/xcodebuild.Rd index 4a66f26..e278548 100644 --- a/man/xcodebuild.Rd +++ b/man/xcodebuild.Rd @@ -1,5 +1,5 @@ % Generated by roxygen2: do not edit by hand -% Please edit documentation in R/xcodebuild.R +% Please edit documentation in R/xcode.R \name{xcodebuild} \alias{xcodebuild} \alias{xcodebuild_version} diff --git a/tests/testthat/test-assertions.R b/tests/testthat/test-assertions.R new file mode 100644 index 0000000..40abc77 --- /dev/null +++ b/tests/testthat/test-assertions.R @@ -0,0 +1,78 @@ +test_that("assert succeeds when condition is TRUE", { + expect_no_error(assert(TRUE, "This should not error")) +}) + +test_that("assert throws error when condition is FALSE", { + expect_error(assert(FALSE, "Error message"), regexp = "Error message") +}) + +test_that("assert_mac succeeds on macOS", { + mockery::stub(assert_mac, "is_macos", function() TRUE) + expect_no_error(assert_mac()) +}) + +test_that("assert_mac throws error on non-macOS", { + mockery::stub(assert_mac, "is_macos", function() FALSE) + mockery::stub(assert_mac, "base::Sys.info", function() c(sysname = "Linux")) + expect_error(assert_mac(), regexp = "This function requires macOS") +}) + +test_that("assert_macos_supported succeeds on supported macOS version", { + # Create a stub that correctly handles the call parameter + mockery::stub(assert_macos_supported, "assert_mac", function(...) NULL) + mockery::stub(assert_macos_supported, "is_macos_r_supported", function() TRUE) + expect_no_error(assert_macos_supported()) +}) + +test_that("assert_macos_supported throws error on unsupported macOS version", { + # Create a stub that correctly handles the call parameter + mockery::stub(assert_macos_supported, "assert_mac", function(...) NULL) + mockery::stub(assert_macos_supported, "is_macos_r_supported", function() FALSE) + mockery::stub(assert_macos_supported, "shell_mac_version", function() "10.12") + # Mock cli::cli_abort to track error message but not actually throw + mockery::stub(assert_macos_supported, "cli::cli_abort", + function(message, ...) stop(paste(message[1], collapse=" "))) + + expect_error(assert_macos_supported(), regexp = "not supported") +}) + +test_that("assert_aarch64 succeeds on Apple Silicon", { + mockery::stub(assert_aarch64, "is_aarch64", function() TRUE) + expect_no_error(assert_aarch64()) +}) + +test_that("assert_aarch64 throws error on non-Apple Silicon", { + mockery::stub(assert_aarch64, "is_aarch64", function() FALSE) + mockery::stub(assert_aarch64, "system_arch", function() "x86_64") + expect_error(assert_aarch64(), regexp = "requires an Apple Silicon") +}) + +test_that("assert_x86_64 succeeds on Intel", { + mockery::stub(assert_x86_64, "is_x86_64", function() TRUE) + expect_no_error(assert_x86_64()) +}) + +test_that("assert_x86_64 throws error on non-Intel", { + mockery::stub(assert_x86_64, "is_x86_64", function() FALSE) + mockery::stub(assert_x86_64, "system_arch", function() "aarch64") + expect_error(assert_x86_64(), regexp = "requires an Intel-based Mac") +}) + +test_that("assert_r_version_supported succeeds on supported R version", { + # Instead of mocking R.version, mock is_r_version to return the expected values + mockery::stub(assert_r_version_supported, "is_r_version", + function(version) version %in% c("4.0", "4.1", "4.2", "4.3", "4.4")) + expect_no_error(assert_r_version_supported()) +}) + +test_that("assert_r_version_supported throws error on unsupported R version", { + # Mock is_r_version to return FALSE for any input + mockery::stub(assert_r_version_supported, "is_r_version", function(...) FALSE) + # Instead of trying to mock R.version, mock paste to return a known version + mockery::stub(assert_r_version_supported, "base::paste", function(...) "3.6.0") + # Mock cli::cli_abort to track error message but not actually throw + mockery::stub(assert_r_version_supported, "cli::cli_abort", + function(message, ...) stop(paste(message[1], collapse=" "))) + + expect_error(assert_r_version_supported(), regexp = "not supported") +}) diff --git a/tests/testthat/test-gfortran.R b/tests/testthat/test-gfortran.R new file mode 100644 index 0000000..24e7a1d --- /dev/null +++ b/tests/testthat/test-gfortran.R @@ -0,0 +1,134 @@ +test_that("is_gfortran_installed correctly identifies installed gfortran", { + # Mock dependencies for success scenario + mockery::stub(is_gfortran_installed, "assert_mac", function() NULL) + mockery::stub(is_gfortran_installed, "gfortran_install_location", function() "/opt") + mockery::stub(is_gfortran_installed, "base::file.path", function(...) "/opt/gfortran") + mockery::stub(is_gfortran_installed, "base::dir.exists", function(path) TRUE) + + expect_true(is_gfortran_installed()) + + # Mock failure scenario + mockery::stub(is_gfortran_installed, "base::dir.exists", function(path) FALSE) + expect_false(is_gfortran_installed()) +}) + +test_that("gfortran_version returns correct output", { + mock_output <- "GNU Fortran (GCC) 9.3.0" + mockery::stub(gfortran_version, "gfortran", function(...) { + base::structure( + base::list( + output = mock_output, + status = 0L + ), + class = c("gfortran", "cli") + ) + }) + + result <- gfortran_version() + expect_equal(result$output, mock_output) + expect_equal(result$status, 0L) +}) + +test_that("gfortran_install skips when already installed", { + # Mock dependencies + mockery::stub(gfortran_install, "assert_mac", function() NULL) + mockery::stub(gfortran_install, "assert_macos_supported", function() NULL) + mockery::stub(gfortran_install, "assert_r_version_supported", function() NULL) + mockery::stub(gfortran_install, "is_gfortran_installed", function() TRUE) + mockery::stub(gfortran_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_bullets", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_text", function(...) NULL) + mockery::stub(gfortran_install, "base::tryCatch", function(...) "Mock version") + mockery::stub(gfortran_install, "base::file.path", function(...) "/opt/gfortran") + + result <- gfortran_install(verbose = TRUE) + expect_true(result) +}) + +test_that("gfortran_install installs correct version for R 4.3+", { + # Mock dependencies + mockery::stub(gfortran_install, "assert_mac", function() NULL) + mockery::stub(gfortran_install, "assert_macos_supported", function() NULL) + mockery::stub(gfortran_install, "assert_r_version_supported", function() NULL) + mockery::stub(gfortran_install, "is_gfortran_installed", function() FALSE) + mockery::stub(gfortran_install, "is_r_version", function(v) v == "4.3") + mockery::stub(gfortran_install, "gfortran_install_location", function() "/opt") + mockery::stub(gfortran_install, "base::file.path", function(...) "/opt/gfortran/bin") + mockery::stub(gfortran_install, "base::paste0", function(...) "$PATH:/opt/gfortran/bin") + mockery::stub(gfortran_install, "force_password", function(pw) "mockpw") + mockery::stub(gfortran_install, "create_install_location", function(...) TRUE) + mockery::stub(gfortran_install, "install_gfortran_12_2_universal", function(...) TRUE) + mockery::stub(gfortran_install, "renviron_gfortran_path", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_bullets", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_text", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_alert_success", function(...) NULL) + + result <- gfortran_install(verbose = TRUE) + expect_true(result) +}) + +test_that("gfortran_install handles Intel Mac with R 4.2", { + # Mock dependencies + mockery::stub(gfortran_install, "assert_mac", function() NULL) + mockery::stub(gfortran_install, "assert_macos_supported", function() NULL) + mockery::stub(gfortran_install, "assert_r_version_supported", function() NULL) + mockery::stub(gfortran_install, "is_gfortran_installed", function() FALSE) + mockery::stub(gfortran_install, "is_r_version", function(v) v == "4.2") + mockery::stub(gfortran_install, "is_x86_64", function() TRUE) + mockery::stub(gfortran_install, "is_aarch64", function() FALSE) + mockery::stub(gfortran_install, "gfortran_install_location", function() "/usr/local") + mockery::stub(gfortran_install, "base::file.path", function(...) "/usr/local/gfortran/bin") + mockery::stub(gfortran_install, "base::paste0", function(...) "$PATH:/usr/local/gfortran/bin") + mockery::stub(gfortran_install, "force_password", function(pw) "mockpw") + mockery::stub(gfortran_install, "create_install_location", function(...) TRUE) + mockery::stub(gfortran_install, "install_gfortran_82_mojave", function(...) TRUE) + mockery::stub(gfortran_install, "renviron_gfortran_path", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_bullets", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_text", function(...) NULL) + mockery::stub(gfortran_install, "cli::cli_alert_success", function(...) NULL) + + result <- gfortran_install(verbose = TRUE) + expect_true(result) +}) + +test_that("gfortran_uninstall succeeds when not installed", { + # Mock dependencies + mockery::stub(gfortran_uninstall, "assert_mac", function() NULL) + mockery::stub(gfortran_uninstall, "is_gfortran_installed", function() FALSE) + mockery::stub(gfortran_uninstall, "cli::cli_alert_info", function(...) NULL) + mockery::stub(gfortran_uninstall, "cli::cli_text", function(...) NULL) + + result <- gfortran_uninstall(verbose = TRUE) + expect_true(result) +}) + +test_that("gfortran_uninstall removes installed gfortran", { + # Mock dependencies with a proper file.path mock + mockery::stub(gfortran_uninstall, "assert_mac", function() NULL) + mockery::stub(gfortran_uninstall, "is_gfortran_installed", function() TRUE) + mockery::stub(gfortran_uninstall, "gfortran_install_location", function() "/opt") + + # Create a more flexible file.path mock + file_path_mock <- function(...) { + args <- list(...) + if (length(args) == 2 && args[[2]] == "gfortran") { + return("/opt/gfortran") + } else if (length(args) == 3 && args[[2]] == "bin" && args[[3]] == "gfortran") { + return("/opt/bin/gfortran") + } + return(do.call(base::file.path, args)) + } + mockery::stub(gfortran_uninstall, "base::file.path", file_path_mock) + + mockery::stub(gfortran_uninstall, "base::paste0", function(...) "rm -rf /opt/gfortran /opt/bin/gfortran") + mockery::stub(gfortran_uninstall, "shell_execute", function(...) 0L) + mockery::stub(gfortran_uninstall, "cli::cli_alert_info", function(...) NULL) + mockery::stub(gfortran_uninstall, "cli::cli_bullets", function(...) NULL) + mockery::stub(gfortran_uninstall, "cli::cli_text", function(...) NULL) + mockery::stub(gfortran_uninstall, "cli::cli_alert_success", function(...) NULL) + + result <- gfortran_uninstall(verbose = TRUE) + expect_true(result) +}) diff --git a/tests/testthat/test-installers.R b/tests/testthat/test-installers.R new file mode 100644 index 0000000..3065107 --- /dev/null +++ b/tests/testthat/test-installers.R @@ -0,0 +1,99 @@ +test_that("binary_download handles successful downloads", { + # Mock dependencies + mockery::stub(binary_download, "base::tempdir", function() "/tmp") + mockery::stub(binary_download, "base::file.path", function(...) "/tmp/test.tar.gz") + mockery::stub(binary_download, "cli::cli_alert_info", function(...) NULL) + mockery::stub(binary_download, "cli::cli_bullets", function(...) NULL) + mockery::stub(binary_download, "cli::cli_text", function(...) NULL) + mockery::stub(binary_download, "cli::cli_progress_bar", function(...) 1) + mockery::stub(binary_download, "cli::cli_progress_done", function(...) NULL) + mockery::stub(binary_download, "cli::cli_alert_success", function(...) NULL) + mockery::stub(binary_download, "base::Sys.time", function() Sys.time()) + mockery::stub(binary_download, "base::tryCatch", + function(expr, ...) { + # Simulate successful download + return(TRUE) + }) + mockery::stub(binary_download, "base::file.info", function(...) list(size = 1024 * 1024)) + mockery::stub(binary_download, "base::round", function(...) 1) + mockery::stub(binary_download, "base::as.numeric", function(...) 10) + + result <- binary_download("https://example.com/test.tar.gz", verbose = TRUE) + expect_equal(result, "/tmp/test.tar.gz") +}) + +test_that("binary_download handles download errors", { + # Mock dependencies with error + mockery::stub(binary_download, "base::tempdir", function() "/tmp") + mockery::stub(binary_download, "base::file.path", function(...) "/tmp/test.tar.gz") + mockery::stub(binary_download, "cli::cli_alert_info", function(...) NULL) + mockery::stub(binary_download, "cli::cli_bullets", function(...) NULL) + mockery::stub(binary_download, "cli::cli_text", function(...) NULL) + mockery::stub(binary_download, "cli::cli_progress_bar", function(...) 1) + mockery::stub(binary_download, "cli::cli_progress_done", function(...) NULL) + mockery::stub(binary_download, "cli::cli_abort", function(...) stop("Download failed")) + mockery::stub(binary_download, "base::tryCatch", + function(expr, error, ...) { + # Simulate download error + error(simpleError("Connection failed")) + }) + + expect_error(binary_download("https://example.com/test.tar.gz", verbose = TRUE), + "Download failed") +}) + +test_that("tar_package_install handles successful installation", { + # Mock dependencies + mockery::stub(tar_package_install, "base::shQuote", function(x) paste0("'", x, "'")) + mockery::stub(tar_package_install, "base::normalizePath", function(path) path) + mockery::stub(tar_package_install, "base::basename", function(path) "test.tar.gz") + mockery::stub(tar_package_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(tar_package_install, "cli::cli_bullets", function(...) NULL) + mockery::stub(tar_package_install, "cli::cli_text", function(...) NULL) + mockery::stub(tar_package_install, "shell_execute", function(...) 0) + mockery::stub(tar_package_install, "base::unlink", function(...) NULL) + mockery::stub(tar_package_install, "cli::cli_alert_success", function(...) NULL) + + result <- tar_package_install("/tmp/test.tar.gz", "/opt", 2, verbose = TRUE) + expect_true(result) +}) + +test_that("tar_package_install handles installation errors", { + # Mock dependencies with error + mockery::stub(tar_package_install, "base::shQuote", function(x) paste0("'", x, "'")) + mockery::stub(tar_package_install, "base::normalizePath", function(path) path) + mockery::stub(tar_package_install, "base::basename", function(path) "test.tar.gz") + mockery::stub(tar_package_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(tar_package_install, "cli::cli_bullets", function(...) NULL) + mockery::stub(tar_package_install, "cli::cli_text", function(...) NULL) + mockery::stub(tar_package_install, "shell_execute", function(...) -1) + mockery::stub(tar_package_install, "cli::cli_abort", function(...) stop("Installation failed")) + + expect_error(tar_package_install("/tmp/test.tar.gz", "/opt", 2, verbose = TRUE), + "Installation failed") +}) + +test_that("create_install_location succeeds when directory exists", { + # Mock dependencies + mockery::stub(create_install_location, "install_location", function(...) "/opt/test") + mockery::stub(create_install_location, "base::dir.exists", function(...) TRUE) + + result <- create_install_location() + expect_true(result) +}) + +test_that("pkg_install handles successful installation", { + # Mock dependencies + mockery::stub(pkg_install, "base::basename", function(path) "test.pkg") + mockery::stub(pkg_install, "tools::file_path_sans_ext", function(path) "test") + mockery::stub(pkg_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(pkg_install, "cli::cli_bullets", function(...) NULL) + mockery::stub(pkg_install, "cli::cli_text", function(...) NULL) + mockery::stub(pkg_install, "base::paste", function(...) "sudo -kS installer -pkg test.pkg -target /") + mockery::stub(pkg_install, "shell_execute", function(...) 0) + mockery::stub(pkg_install, "cli::cli_alert_success", function(...) NULL) + mockery::stub(pkg_install, "cli::cli_abort", function(...) NULL) + + result <- pkg_install("/tmp/test.pkg", verbose = TRUE) + expect_true(result) +}) diff --git a/tests/testthat/test-shell.R b/tests/testthat/test-shell.R new file mode 100644 index 0000000..34312f0 --- /dev/null +++ b/tests/testthat/test-shell.R @@ -0,0 +1,78 @@ +test_that("shell_command executes commands correctly", { + # Mock dependencies + mockery::stub(shell_command, "cli::cli_alert_info", function(...) NULL) + mockery::stub(shell_command, "cli::cli_bullets", function(...) NULL) + mockery::stub(shell_command, "cli::cli_text", function(...) NULL) + mockery::stub(shell_command, "base::getwd", function() "/home/user") + mockery::stub(shell_command, "base::system", function(cmd) { + if (cmd == "echo test") return(0) + return(1) + }) + + # Test successful command + result <- shell_command("echo test", verbose = TRUE) + expect_equal(result, 0) + + # Test failing command + result <- shell_command("invalid_command", verbose = TRUE) + expect_equal(result, 1) +}) + +test_that("shell_sudo_command executes privileged commands correctly", { + # Mock dependencies + mockery::stub(shell_sudo_command, "cli::cli_alert_info", function(...) NULL) + mockery::stub(shell_sudo_command, "cli::cli_bullets", function(...) NULL) + mockery::stub(shell_sudo_command, "cli::cli_text", function(...) NULL) + mockery::stub(shell_sudo_command, "cli::cli_alert_warning", function(...) NULL) + mockery::stub(shell_sudo_command, "base::system", function(cmd, input) { + if (grepl("echo test", cmd) && input == "password") return(0) + return(1) + }) + + # Test successful command with password + result <- shell_sudo_command("echo test", password = "password", verbose = TRUE) + expect_equal(result, 0) + + # Test failing command + result <- shell_sudo_command("invalid_command", password = "password", verbose = TRUE) + expect_equal(result, 1) + + # Test with askpass + mockery::stub(shell_sudo_command, "askpass::askpass", function(...) "password") + result <- shell_sudo_command("echo test", password = NULL, verbose = TRUE) + expect_equal(result, 0) +}) + +test_that("shell_execute routes commands correctly", { + # Mock dependencies + mockery::stub(shell_execute, "cli::cli_alert_info", function(...) NULL) + mockery::stub(shell_execute, "cli::cli_code", function(...) NULL) + mockery::stub(shell_execute, "cli::cli_text", function(...) NULL) + mockery::stub(shell_execute, "cli::cli_alert_success", function(...) NULL) + mockery::stub(shell_execute, "cli::cli_alert_warning", function(...) NULL) + mockery::stub(shell_execute, "cli::cli_bullets", function(...) NULL) + + # Test regular command + mockery::stub(shell_execute, "shell_command", function(cmd, verbose) { + if (cmd == "echo test") return(0) + return(1) + }) + mockery::stub(shell_execute, "shell_sudo_command", function(cmd, password, verbose) 1) + mockery::stub(shell_execute, "base::Sys.time", function() Sys.time()) + mockery::stub(shell_execute, "base::difftime", function(...) 1) + mockery::stub(shell_execute, "base::round", function(...) 1) + mockery::stub(shell_execute, "base::as.numeric", function(...) 1) + + result <- shell_execute("echo test", sudo = FALSE, verbose = TRUE) + expect_equal(result, 0) + + # Test sudo command + mockery::stub(shell_execute, "shell_command", function(cmd, verbose) 1) + mockery::stub(shell_execute, "shell_sudo_command", function(cmd, password, verbose) { + if (cmd == "echo test" && password == "password") return(0) + return(1) + }) + + result <- shell_execute("echo test", sudo = TRUE, password = "password", verbose = TRUE) + expect_equal(result, 0) +}) diff --git a/tests/testthat/test-system.R b/tests/testthat/test-system.R new file mode 100644 index 0000000..5a2baa0 --- /dev/null +++ b/tests/testthat/test-system.R @@ -0,0 +1,81 @@ +test_that("system_os returns the correct OS name", { + mockery::stub(system_os, "base::Sys.info", function() c(sysname = "Darwin")) + expect_equal(system_os(), "darwin") + + mockery::stub(system_os, "base::Sys.info", function() c(sysname = "Linux")) + expect_equal(system_os(), "linux") +}) + +test_that("system_arch returns the correct architecture", { + # Instead of trying to mock R.version directly, we test the function itself + expected_arch <- base::R.version$arch + expect_equal(system_arch(), expected_arch) + + # For additional coverage, we test that the functions that use system_arch work + mockery::stub(is_aarch64, "system_arch", function() "aarch64") + expect_true(is_aarch64()) + + mockery::stub(is_x86_64, "system_arch", function() "x86_64") + expect_true(is_x86_64()) +}) + +test_that("is_macos correctly identifies macOS", { + mockery::stub(is_macos, "system_os", function() "darwin") + expect_true(is_macos()) + + mockery::stub(is_macos, "system_os", function() "linux") + expect_false(is_macos()) +}) + +test_that("shell_mac_version returns the correct macOS version", { + mockery::stub(shell_mac_version, "sys::exec_internal", function(...) { + list(stdout = charToRaw("14.0")) + }) + mockery::stub(shell_mac_version, "sys::as_text", function(x) "14.0") + + expect_equal(shell_mac_version(), "14.0") +}) + +test_that("is_macos_r_supported correctly identifies supported macOS versions", { + mockery::stub(is_macos_r_supported, "shell_mac_version", function() "10.13.0") + mockery::stub(is_macos_r_supported, "version_between", function(...) TRUE) + expect_true(is_macos_r_supported()) + + mockery::stub(is_macos_r_supported, "shell_mac_version", function() "10.12.0") + mockery::stub(is_macos_r_supported, "version_between", function(...) FALSE) + expect_false(is_macos_r_supported()) +}) + +test_that("version_between correctly determines if version is within bounds", { + expect_true(version_between("10.14.0", "10.13.0", "10.15.0")) + expect_true(version_between("10.13.0", "10.13.0", "10.15.0")) + expect_false(version_between("10.15.0", "10.13.0", "10.15.0")) + expect_false(version_between("10.12.0", "10.13.0", "10.15.0")) +}) + +test_that("is_r_version correctly identifies R versions", { + # We can't easily mock R.version, so instead we test a simplified version + # of the function that uses mock data + simplified_is_r_version <- function(target_version, r_major = "4", r_minor = "2.1") { + minor_value <- strsplit(r_minor, ".", fixed = TRUE)[[1]][1] + version_string <- paste(r_major, minor_value, sep = ".") + return(version_string == target_version) + } + + expect_true(simplified_is_r_version("4.2")) + expect_false(simplified_is_r_version("4.1")) + + # Test with compare_major_minor = FALSE (directly using full version) + simplified_is_r_version_full <- function(target_version, r_major = "4", r_minor = "2.1", compare_major_minor = FALSE) { + if (compare_major_minor) { + minor_value <- strsplit(r_minor, ".", fixed = TRUE)[[1]][1] + } else { + minor_value <- r_minor + } + version_string <- paste(r_major, minor_value, sep = ".") + return(version_string == target_version) + } + + expect_true(simplified_is_r_version_full("4.2.1", compare_major_minor = FALSE)) + expect_false(simplified_is_r_version_full("4.2.0", compare_major_minor = FALSE)) +}) diff --git a/tests/testthat/test-toolchain.R b/tests/testthat/test-toolchain.R new file mode 100644 index 0000000..38b7d44 --- /dev/null +++ b/tests/testthat/test-toolchain.R @@ -0,0 +1,162 @@ +test_that("macos_rtools_install performs system checks first", { + # Mock system checks + mockery::stub(macos_rtools_install, "assert_mac", function() stop("Not macOS")) + + # CLI mocks to avoid output + mockery::stub(macos_rtools_install, "cli::cli_h3", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_text", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_ul", function(...) NULL) + + expect_error(macos_rtools_install(), "Not macOS") + + # Reset mocks for assert_mac to pass but fail on next check + mockery::stub(macos_rtools_install, "assert_mac", function() NULL) + mockery::stub(macos_rtools_install, "assert_macos_supported", function() stop("Unsupported macOS")) + + expect_error(macos_rtools_install(), "Unsupported macOS") +}) + +test_that("macos_rtools_install handles component installations", { + # Mock all system checks + mockery::stub(macos_rtools_install, "assert_mac", function() NULL) + mockery::stub(macos_rtools_install, "assert_macos_supported", function() NULL) + mockery::stub(macos_rtools_install, "assert_r_version_supported", function() NULL) + + # Mock system info gathering + mockery::stub(macos_rtools_install, "shell_mac_version", function() "14.0") + mockery::stub(macos_rtools_install, "system_arch", function() "aarch64") + mockery::stub(macos_rtools_install, "base::Sys.info", function() c(release = "23.0")) + mockery::stub(macos_rtools_install, "base::R.version", list(major = "4", minor = "3")) + mockery::stub(macos_rtools_install, "base::paste", function(...) "4.3") + mockery::stub(macos_rtools_install, "base::tryCatch", function(...) 10) + mockery::stub(macos_rtools_install, "base::round", function(...) 10) + mockery::stub(macos_rtools_install, "base::as.numeric", function(...) 10) + mockery::stub(macos_rtools_install, "base::format", function(...) "2023-01-01 12:00:00") + mockery::stub(macos_rtools_install, "base::Sys.time", function() Sys.time()) + + # Mock all CLI functions + mockery::stub(macos_rtools_install, "cli::cli_h3", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_text", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_ul", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_bullets", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_progress_bar", function(...) 1) + mockery::stub(macos_rtools_install, "cli::cli_progress_update", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_progress_done", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_alert_success", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_code", function(...) NULL) + + # Mock password entry + mockery::stub(macos_rtools_install, "askpass::askpass", function(...) "password") + + # Mock Xcode installation + mockery::stub(macos_rtools_install, "is_xcode_app_installed", function() FALSE) + mockery::stub(macos_rtools_install, "is_xcode_cli_installed", function() FALSE) + mockery::stub(macos_rtools_install, "xcode_cli_install", function(...) TRUE) + + # Mock gfortran installation + mockery::stub(macos_rtools_install, "is_gfortran_installed", function() FALSE) + mockery::stub(macos_rtools_install, "gfortran_install", function(...) TRUE) + + # Mock recipe installation + mockery::stub(macos_rtools_install, "recipe_binary_install_location", function(...) "/opt/R/arm64") + mockery::stub(macos_rtools_install, "recipes_binary_install", function(...) TRUE) + + # Mock version info for summary + mockery::stub(macos_rtools_install, "base::tryCatch", function(...) "Mock version info") + mockery::stub(macos_rtools_install, "base::substr", function(...) "Mock version") + mockery::stub(macos_rtools_install, "base::paste0", function(...) "Mock version...") + mockery::stub(macos_rtools_install, "base::nchar", function(...) 30) + + result <- macos_rtools_install(verbose = TRUE) + expect_true(result) +}) + +test_that("macos_rtools_install handles component failures", { + # Mock all system checks + mockery::stub(macos_rtools_install, "assert_mac", function() NULL) + mockery::stub(macos_rtools_install, "assert_macos_supported", function() NULL) + mockery::stub(macos_rtools_install, "assert_r_version_supported", function() NULL) + + # Mock system info gathering + mockery::stub(macos_rtools_install, "shell_mac_version", function() "14.0") + mockery::stub(macos_rtools_install, "system_arch", function() "aarch64") + mockery::stub(macos_rtools_install, "base::Sys.info", function() c(release = "23.0")) + mockery::stub(macos_rtools_install, "base::R.version", list(major = "4", minor = "3")) + mockery::stub(macos_rtools_install, "base::paste", function(...) "4.3") + mockery::stub(macos_rtools_install, "base::tryCatch", function(...) 10) + mockery::stub(macos_rtools_install, "base::round", function(...) 10) + mockery::stub(macos_rtools_install, "base::as.numeric", function(...) 10) + + # Mock all CLI functions + mockery::stub(macos_rtools_install, "cli::cli_h3", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_text", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_ul", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_bullets", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_progress_bar", function(...) 1) + mockery::stub(macos_rtools_install, "cli::cli_progress_update", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_progress_done", function(...) NULL) + mockery::stub(macos_rtools_install, "cli::cli_abort", function(...) stop("Installation failed")) + + # Mock component failures + mockery::stub(macos_rtools_install, "is_xcode_app_installed", function() FALSE) + mockery::stub(macos_rtools_install, "is_xcode_cli_installed", function() FALSE) + mockery::stub(macos_rtools_install, "xcode_cli_install", function(...) FALSE) + mockery::stub(macos_rtools_install, "is_gfortran_installed", function() FALSE) + mockery::stub(macos_rtools_install, "gfortran_install", function(...) FALSE) + mockery::stub(macos_rtools_install, "recipes_binary_install", function(...) FALSE) + + expect_error(macos_rtools_install(verbose = TRUE), "Installation failed") +}) + +test_that("macos_rtools_uninstall handles component uninstallations", { + # Mock CLI functions + mockery::stub(macos_rtools_uninstall, "cli::cli_alert_info", function(...) NULL) + mockery::stub(macos_rtools_uninstall, "cli::cli_bullets", function(...) NULL) + mockery::stub(macos_rtools_uninstall, "cli::cli_text", function(...) NULL) + mockery::stub(macos_rtools_uninstall, "askpass::askpass", function(...) "password") + mockery::stub(macos_rtools_uninstall, "cli::cli_progress_bar", function(...) 1) + mockery::stub(macos_rtools_uninstall, "cli::cli_progress_update", function(...) NULL) + mockery::stub(macos_rtools_uninstall, "cli::cli_progress_done", function(...) NULL) + mockery::stub(macos_rtools_uninstall, "cli::cli_alert_success", function(...) NULL) + + # Mock component detection and uninstallation + mockery::stub(macos_rtools_uninstall, "is_xcode_cli_installed", function() TRUE) + mockery::stub(macos_rtools_uninstall, "xcode_cli_uninstall", function(...) TRUE) + mockery::stub(macos_rtools_uninstall, "is_gfortran_installed", function() TRUE) + mockery::stub(macos_rtools_uninstall, "gfortran_uninstall", function(...) TRUE) + + result <- macos_rtools_uninstall(verbose = TRUE) + expect_true(result) + + # Test when components are not installed + mockery::stub(macos_rtools_uninstall, "is_xcode_cli_installed", function() FALSE) + mockery::stub(macos_rtools_uninstall, "is_gfortran_installed", function() FALSE) + + result <- macos_rtools_uninstall(verbose = TRUE) + expect_true(result) +}) + +test_that("macos_rtools_uninstall handles component failures", { + # Mock CLI functions + mockery::stub(macos_rtools_uninstall, "cli::cli_alert_info", function(...) NULL) + mockery::stub(macos_rtools_uninstall, "cli::cli_bullets", function(...) NULL) + mockery::stub(macos_rtools_uninstall, "cli::cli_text", function(...) NULL) + mockery::stub(macos_rtools_uninstall, "cli::cli_progress_bar", function(...) 1) + mockery::stub(macos_rtools_uninstall, "cli::cli_progress_update", function(...) NULL) + mockery::stub(macos_rtools_uninstall, "cli::cli_abort", function(...) stop("Uninstallation failed")) + + # Mock component detection and uninstallation failure + mockery::stub(macos_rtools_uninstall, "is_xcode_cli_installed", function() TRUE) + mockery::stub(macos_rtools_uninstall, "xcode_cli_uninstall", function(...) FALSE) + + expect_error(macos_rtools_uninstall(verbose = TRUE), "Uninstallation failed") + + # Test when gfortran fails + mockery::stub(macos_rtools_uninstall, "is_xcode_cli_installed", function() FALSE) + mockery::stub(macos_rtools_uninstall, "is_gfortran_installed", function() TRUE) + mockery::stub(macos_rtools_uninstall, "gfortran_uninstall", function(...) FALSE) + + expect_error(macos_rtools_uninstall(verbose = TRUE), "Uninstallation failed") +}) diff --git a/tests/testthat/test-xcode.R b/tests/testthat/test-xcode.R new file mode 100644 index 0000000..ad757e1 --- /dev/null +++ b/tests/testthat/test-xcode.R @@ -0,0 +1,152 @@ +test_that("xcode_select_path returns correct output", { + # Mock successful xcode-select call + mock_output <- list( + stdout = charToRaw("/Library/Developer/CommandLineTools"), + stderr = charToRaw(""), + status = 0L + ) + + mockery::stub(xcode_select_path, "xcode_select", function(...) { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + }) + + result <- xcode_select_path() + expect_equal(result$status, 0L) + expect_equal(result$output, "/Library/Developer/CommandLineTools") +}) + +test_that("is_xcode_cli_installed correctly identifies installed CLI tools", { + # First, mock dependencies for success scenario + mockery::stub(is_xcode_cli_installed, "assert_mac", function() TRUE) + + # Mock successful path detection + mockery::stub(is_xcode_cli_installed, "xcode_select_path", function() { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + }) + + # Mock installation directory exists + mockery::stub(is_xcode_cli_installed, "install_directory_xcode_cli", function() "/Library/Developer/CommandLineTools") + mockery::stub(is_xcode_cli_installed, "base::dir.exists", function(path) TRUE) + + expect_true(is_xcode_cli_installed()) + + # Now mock failure scenarios + mockery::stub(is_xcode_cli_installed, "assert_mac", function() TRUE) + mockery::stub(is_xcode_cli_installed, "xcode_select_path", function() { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", + error = "", + status = 1L # Non-zero status + ), + class = c("xcodeselect", "cli") + ) + }) + + expect_false(is_xcode_cli_installed()) +}) + +test_that("is_xcode_app_installed correctly identifies installed Xcode app", { + # Mock dependencies for success scenario + mockery::stub(is_xcode_app_installed, "assert_mac", function() TRUE) + + # Mock successful path detection + mockery::stub(is_xcode_app_installed, "xcode_select_path", function() { + base::structure( + base::list( + output = "/Applications/Xcode.app/Contents/Developer", + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + }) + + # Mock installation directory exists + mockery::stub(is_xcode_app_installed, "install_directory_xcode_app", function() "/Applications/Xcode.app/Contents/Developer") + mockery::stub(is_xcode_app_installed, "base::dir.exists", function(path) TRUE) + + expect_true(is_xcode_app_installed()) + + # Now mock failure scenarios + mockery::stub(is_xcode_app_installed, "xcode_select_path", function() { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", # Wrong path + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + }) + + expect_false(is_xcode_app_installed()) +}) + +test_that("xcode_cli_path returns correct path", { + # Mock successful xcode-select call + mockery::stub(xcode_cli_path, "xcode_select_path", function() { + base::structure( + base::list( + output = "/Library/Developer/CommandLineTools", + error = "", + status = 0L + ), + class = c("xcodeselect", "cli") + ) + }) + + expect_equal(xcode_cli_path(), "/Library/Developer/CommandLineTools") + + # Mock failed xcode-select call + mockery::stub(xcode_cli_path, "xcode_select_path", function() { + base::structure( + base::list( + output = "", + error = "Error: command not found", + status = 1L + ), + class = c("xcodeselect", "cli") + ) + }) + + expect_equal(xcode_cli_path(), "") +}) + +test_that("xcode_cli_install skips when already installed", { + # Mock successful check + mockery::stub(xcode_cli_install, "assert_mac", function() TRUE) + mockery::stub(xcode_cli_install, "is_xcode_cli_installed", function() TRUE) + mockery::stub(xcode_cli_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(xcode_cli_install, "cli::cli_text", function(...) NULL) + + result <- xcode_cli_install(verbose = TRUE) + expect_true(result) +}) + +test_that("xcode_cli_install skips when Xcode app is installed", { + # Mock CLI not installed but Xcode app is + mockery::stub(xcode_cli_install, "assert_mac", function() TRUE) + mockery::stub(xcode_cli_install, "is_xcode_cli_installed", function() FALSE) + mockery::stub(xcode_cli_install, "is_xcode_app_installed", function() TRUE) + mockery::stub(xcode_cli_install, "cli::cli_alert_info", function(...) NULL) + mockery::stub(xcode_cli_install, "cli::cli_bullets", function(...) NULL) + mockery::stub(xcode_cli_install, "cli::cli_text", function(...) NULL) + + result <- xcode_cli_install(verbose = TRUE) + expect_true(result) +})