From bca2686c0bab2ca750c40795dbbb3b118f36412a Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Mon, 8 Jun 2026 22:50:08 -0400 Subject: [PATCH 1/3] Add macOS PATH setup for CLI launchers --- NEWS.md | 3 + R/install.R | 92 ++++++++++++- README.md | 9 +- man/install_pkg_cli_apps.Rd | 7 +- tests/testthat/test-install.R | 244 ++++++++++++++++++++++++++++++++++ 5 files changed, 345 insertions(+), 10 deletions(-) diff --git a/NEWS.md b/NEWS.md index 8b62627..1d07282 100644 --- a/NEWS.md +++ b/NEWS.md @@ -18,6 +18,9 @@ ## Bug fixes +- On macOS, `install_pkg_cli_apps()` now adds the default `~/.local/bin` + install directory to `~/.zprofile` when it is not already on `PATH` + (#35). - Launcher front matter now accepts documented kebab-case option names such as `default-packages`. Installation docs now clarify that package apps are discovered as `exec/*.R`, installed without the `.R` extension diff --git a/R/install.R b/R/install.R index faee65b..84925c2 100644 --- a/R/install.R +++ b/R/install.R @@ -50,10 +50,11 @@ #' - Windows: `%LOCALAPPDATA%\Programs\R\Rapp\bin` #' #' On Windows, the resolved `destdir` is explicitly added to `PATH` (it -#' generally is not by default). To disable adding it to the `PATH`, set envvar -#' `RAPP_NO_MODIFY_PATH=1`. +#' generally is not by default). On macOS, when the default `~/.local/bin` +#' destination is not already on `PATH`, it is added to `~/.zprofile`. To +#' disable adding it to the `PATH`, set envvar `RAPP_NO_MODIFY_PATH=1`. #' -#' On macOS or Linux, `~/.local/bin` is typically already on `PATH` if it +#' On Linux, `~/.local/bin` is typically already on `PATH` if it #' exists. Note: some shells add `~/.local/bin` to `PATH` only if it exists at #' login. If `install_pkg_cli_apps()` created the directory, you may need to #' restart the shell for the new apps to be found on `PATH`. @@ -82,6 +83,7 @@ install_pkg_cli_apps <- function( lib.loc = NULL, overwrite = NA ) { + default_destdir <- is.null(destdir) destdir <- destdir %||% rapp_install_dir() dir.exists(destdir) || dir.create(destdir, recursive = TRUE) || @@ -89,6 +91,8 @@ install_pkg_cli_apps <- function( if (is_windows()) { ensure_path_windows(destdir) + } else if (is_macos()) { + ensure_path_macos(destdir, default_destdir = default_destdir) } # existing Rapp launchers we're either overwriting or deleting @@ -518,6 +522,88 @@ get_env_win_registry <- function(name) { utils::readRegistry("Environment", hive = "HCU", view = "default")[[name]] } +ensure_path_macos <- function(destdir = rapp_install_dir(), default_destdir = TRUE) { + if (Sys.getenv("RAPP_NO_MODIFY_PATH") != "") { + return(FALSE) + } + stopifnot(is_macos()) + if (!isTRUE(default_destdir)) { + return(FALSE) + } + + destdir <- normalizePath(destdir, mustWork = TRUE) + if (!identical(destdir, macos_default_install_dir())) { + return(FALSE) + } + if (path_has_dir(destdir)) { + return(FALSE) + } + + zprofile <- path(path.expand("~"), ".zprofile") + lines <- macos_path_lines() + + if (profile_has_lines(zprofile, lines)) { + warning( + "~/.local/bin PATH setup is already present in ~/.zprofile, ", + "but ~/.local/bin is still not on PATH.\n", + "Restart your shell, or run:\n\n", + " source ~/.zprofile", + call. = FALSE + ) + } else { + cat( + paste0(c("", lines), collapse = "\n"), + "\n", + file = zprofile, + append = TRUE, + sep = "" + ) + message( + "Added ~/.local/bin to PATH in ~/.zprofile.\n", + "Restart your shell, or run:\n\n", + " source ~/.zprofile" + ) + } + + Sys.setenv(PATH = paste(destdir, Sys.getenv("PATH"), sep = .Platform$path.sep)) + TRUE +} + +macos_default_install_dir <- function() { + normalizePath(file.path(path.expand("~"), ".local", "bin"), mustWork = FALSE) +} + +macos_path_lines <- function() { + c( + 'case ":$PATH:" in', + ' *:"$HOME/.local/bin":*) ;;', + ' *) export PATH="$HOME/.local/bin:$PATH" ;;', + "esac" + ) +} + +profile_has_lines <- function(profile, lines) { + if (!file.exists(profile)) { + return(FALSE) + } + profile <- paste(readLines(profile, warn = FALSE), collapse = "\n") + grepl(paste(lines, collapse = "\n"), profile, fixed = TRUE) +} + +path_has_dir <- function(dir, env_path = Sys.getenv("PATH")) { + normalizePath(dir, mustWork = FALSE) %in% path_entries(env_path) +} + +path_entries <- function(env_path = Sys.getenv("PATH")) { + entries <- strsplit(env_path, .Platform$path.sep, fixed = TRUE)[[1L]] + entries <- entries[nzchar(entries)] + normalizePath(entries, mustWork = FALSE) +} + +is_macos <- function() { + identical(Sys.info()[["sysname"]], "Darwin") +} + path <- function(...) { normalizePath(file.path(...), mustWork = FALSE) } diff --git a/README.md b/README.md index 9cd90f5..523d253 100644 --- a/README.md +++ b/README.md @@ -425,10 +425,11 @@ App launchers are written to `destdir`, which defaults to the first available location from `RAPP_BIN_DIR`, `XDG_BIN_HOME`, `XDG_DATA_HOME/../bin`, or the default location, `~/.local/bin` on macOS and Linux and `%LOCALAPPDATA%\Programs\R\Rapp\bin` on Windows. On -Windows the directory is automatically added to `PATH`; on macOS and -Linux the directory generally is already present on `PATH` (you may need -to restart your shell if the Rapp installer created the directory). Use -the `destdir` argument if you prefer an alternate location. +Windows the directory is automatically added to `PATH`; on macOS, the +default `~/.local/bin` directory is added to `~/.zprofile` when needed. +On Linux the directory generally is already present on `PATH` (you may +need to log out and back in if the Rapp installer created the directory). +Use the `destdir` argument if you prefer an alternate location. Use `#| launcher:` front matter to customize the installed launcher. For example, `name` changes the installed command name, and `vanilla`, diff --git a/man/install_pkg_cli_apps.Rd b/man/install_pkg_cli_apps.Rd index 53db53f..152cf30 100644 --- a/man/install_pkg_cli_apps.Rd +++ b/man/install_pkg_cli_apps.Rd @@ -72,10 +72,11 @@ If \code{destdir} is not provided, it is resolved in this order: } On Windows, the resolved \code{destdir} is explicitly added to \code{PATH} (it -generally is not by default). To disable adding it to the \code{PATH}, set envvar -\code{RAPP_NO_MODIFY_PATH=1}. +generally is not by default). On macOS, when the default \verb{~/.local/bin} +destination is not already on \code{PATH}, it is added to \verb{~/.zprofile}. To +disable adding it to the \code{PATH}, set envvar \code{RAPP_NO_MODIFY_PATH=1}. -On macOS or Linux, \verb{~/.local/bin} is typically already on \code{PATH} if it +On Linux, \verb{~/.local/bin} is typically already on \code{PATH} if it exists. Note: some shells add \verb{~/.local/bin} to \code{PATH} only if it exists at login. If \code{install_pkg_cli_apps()} created the directory, you may need to restart the shell for the new apps to be found on \code{PATH}. diff --git a/tests/testthat/test-install.R b/tests/testthat/test-install.R index a746903..6bfa002 100644 --- a/tests/testthat/test-install.R +++ b/tests/testthat/test-install.R @@ -213,6 +213,250 @@ test_that("non-Rapp executables respect overwrite flag", { }) +test_that("install_pkg_cli_apps adds default macOS install dir to PATH setup", { + skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) + skip_on_cran() + + home <- tempfile("rapp-home-") + dir.create(home, recursive = TRUE) + withr::local_envvar( + HOME = home, + RAPP_NO_MODIFY_PATH = NA, + RAPP_BIN_DIR = NA, + XDG_BIN_HOME = NA, + XDG_DATA_HOME = NA, + PATH = "/usr/bin" + ) + on.exit(unlink(home, recursive = TRUE), add = TRUE) + + pkg <- "rappTestMacPath" + fake <- setup_fake_rapp_package(tempdir(), "-install-mac-path", package = pkg) + on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE) + + app_path <- file.path(fake[["exec"]], "hello.R") + writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path) + + messages <- character() + created <- withCallingHandlers( + install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]]), + message = function(m) { + messages <<- c(messages, conditionMessage(m)) + invokeRestart("muffleMessage") + } + ) + + destdir <- path(home, ".local", "bin") + expect_same_path(created, path(destdir, "hello")) + expect_true(any(grepl("Added .*\\.local/bin to PATH", messages))) + expect_true(any(grepl("source ~/.zprofile", messages, fixed = TRUE))) + + zprofile <- file.path(home, ".zprofile") + expect_true(file.exists(zprofile)) + expect_identical( + readLines(zprofile), + c( + "", + 'case ":$PATH:" in', + ' *:"$HOME/.local/bin":*) ;;', + ' *) export PATH="$HOME/.local/bin:$PATH" ;;', + "esac" + ) + ) + expect_true(grepl(destdir, Sys.getenv("PATH"), fixed = TRUE)) +}) + + +test_that("install_pkg_cli_apps does not duplicate macOS PATH setup", { + skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) + skip_on_cran() + + home <- tempfile("rapp-home-") + dir.create(home, recursive = TRUE) + withr::local_envvar( + HOME = home, + RAPP_NO_MODIFY_PATH = NA, + RAPP_BIN_DIR = NA, + XDG_BIN_HOME = NA, + XDG_DATA_HOME = NA, + PATH = "/usr/bin" + ) + on.exit(unlink(home, recursive = TRUE), add = TRUE) + + pkg <- "rappTestMacPathTwice" + fake <- setup_fake_rapp_package(tempdir(), "-install-mac-path-twice", package = pkg) + on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE) + + app_path <- file.path(fake[["exec"]], "hello.R") + writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path) + + suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]])) + suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]])) + + zprofile <- file.path(home, ".zprofile") + lines <- readLines(zprofile) + expect_equal(sum(grepl('case ":\\$PATH:" in', lines)), 1L) +}) + + +test_that("install_pkg_cli_apps ignores unrelated macOS profile mentions", { + skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) + skip_on_cran() + + home <- tempfile("rapp-home-") + dir.create(home, recursive = TRUE) + withr::local_envvar( + HOME = home, + RAPP_NO_MODIFY_PATH = NA, + RAPP_BIN_DIR = NA, + XDG_BIN_HOME = NA, + XDG_DATA_HOME = NA, + PATH = "/usr/bin" + ) + on.exit(unlink(home, recursive = TRUE), add = TRUE) + + zprofile <- file.path(home, ".zprofile") + writeLines("# ~/.local/bin is where Rapp writes launchers", zprofile) + + pkg <- "rappTestMacPathComment" + fake <- setup_fake_rapp_package( + tempdir(), + "-install-mac-path-comment", + package = pkg + ) + on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE) + + app_path <- file.path(fake[["exec"]], "hello.R") + writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path) + + suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]])) + + expect_identical( + readLines(zprofile), + c( + "# ~/.local/bin is where Rapp writes launchers", + "", + 'case ":$PATH:" in', + ' *:"$HOME/.local/bin":*) ;;', + ' *) export PATH="$HOME/.local/bin:$PATH" ;;', + "esac" + ) + ) +}) + + +test_that("install_pkg_cli_apps warns when macOS PATH setup already exists but PATH is stale", { + skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) + skip_on_cran() + + home <- tempfile("rapp-home-") + dir.create(home, recursive = TRUE) + withr::local_envvar( + HOME = home, + RAPP_NO_MODIFY_PATH = NA, + RAPP_BIN_DIR = NA, + XDG_BIN_HOME = NA, + XDG_DATA_HOME = NA, + PATH = "/usr/bin" + ) + on.exit(unlink(home, recursive = TRUE), add = TRUE) + + zprofile <- file.path(home, ".zprofile") + writeLines( + c( + 'case ":$PATH:" in', + ' *:"$HOME/.local/bin":*) ;;', + ' *) export PATH="$HOME/.local/bin:$PATH" ;;', + "esac" + ), + zprofile + ) + + pkg <- "rappTestMacPathStale" + fake <- setup_fake_rapp_package( + tempdir(), + "-install-mac-path-stale", + package = pkg + ) + on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE) + + app_path <- file.path(fake[["exec"]], "hello.R") + writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path) + + expect_warning( + suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]])), + "~/.local/bin is still not on PATH" + ) + expect_equal(sum(grepl('case ":\\$PATH:" in', readLines(zprofile))), 1L) +}) + + +test_that("install_pkg_cli_apps leaves macOS profile alone when PATH already includes destdir", { + skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) + skip_on_cran() + + home <- tempfile("rapp-home-") + dir.create(home, recursive = TRUE) + destdir <- path(home, ".local", "bin") + withr::local_envvar( + HOME = home, + RAPP_NO_MODIFY_PATH = NA, + RAPP_BIN_DIR = NA, + XDG_BIN_HOME = NA, + XDG_DATA_HOME = NA, + PATH = paste(destdir, "/usr/bin", sep = .Platform$path.sep) + ) + on.exit(unlink(home, recursive = TRUE), add = TRUE) + + pkg <- "rappTestMacPathPresent" + fake <- setup_fake_rapp_package( + tempdir(), + "-install-mac-path-present", + package = pkg + ) + on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE) + + app_path <- file.path(fake[["exec"]], "hello.R") + writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path) + + suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]])) + + expect_false(file.exists(file.path(home, ".zprofile"))) +}) + + +test_that("install_pkg_cli_apps leaves macOS PATH setup alone when disabled", { + skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) + skip_on_cran() + + home <- tempfile("rapp-home-") + dir.create(home, recursive = TRUE) + withr::local_envvar( + HOME = home, + RAPP_NO_MODIFY_PATH = "1", + RAPP_BIN_DIR = NA, + XDG_BIN_HOME = NA, + XDG_DATA_HOME = NA, + PATH = "/usr/bin" + ) + on.exit(unlink(home, recursive = TRUE), add = TRUE) + + pkg <- "rappTestMacPathDisabled" + fake <- setup_fake_rapp_package( + tempdir(), + "-install-mac-path-disabled", + package = pkg + ) + on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE) + + app_path <- file.path(fake[["exec"]], "hello.R") + writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path) + + suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]])) + + expect_false(file.exists(file.path(home, ".zprofile"))) +}) + + test_that("front matter customises launcher options", { withr::local_envvar(RAPP_NO_MODIFY_PATH = "1") From 7011febb111a3bb4a123c6f6a5889488575f9972 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 07:18:04 -0400 Subject: [PATCH 2/3] Handle macOS profile setup edge cases --- NEWS.md | 5 +- R/install.R | 83 ++++++++++++++++++------- README.md | 10 +-- man/install_pkg_cli_apps.Rd | 6 +- tests/testthat/test-install.R | 112 ++++++++++++++++++++++++++++++++++ 5 files changed, 186 insertions(+), 30 deletions(-) diff --git a/NEWS.md b/NEWS.md index 1d07282..90af59e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -19,8 +19,9 @@ ## Bug fixes - On macOS, `install_pkg_cli_apps()` now adds the default `~/.local/bin` - install directory to `~/.zprofile` when it is not already on `PATH` - (#35). + install directory to the user's zsh profile when it is not already + on `PATH`, respecting `ZDOTDIR` and warning rather than failing if + the profile cannot be updated (#35). - Launcher front matter now accepts documented kebab-case option names such as `default-packages`. Installation docs now clarify that package apps are discovered as `exec/*.R`, installed without the `.R` extension diff --git a/R/install.R b/R/install.R index 84925c2..15ae403 100644 --- a/R/install.R +++ b/R/install.R @@ -51,8 +51,10 @@ #' #' On Windows, the resolved `destdir` is explicitly added to `PATH` (it #' generally is not by default). On macOS, when the default `~/.local/bin` -#' destination is not already on `PATH`, it is added to `~/.zprofile`. To -#' disable adding it to the `PATH`, set envvar `RAPP_NO_MODIFY_PATH=1`. +#' destination is not already on `PATH`, it is added to `~/.zprofile`, or +#' `$ZDOTDIR/.zprofile` when `ZDOTDIR` is set. If the profile cannot be +#' updated, a warning is emitted and installation continues. To disable adding +#' it to the `PATH`, set envvar `RAPP_NO_MODIFY_PATH=1`. #' #' On Linux, `~/.local/bin` is typically already on `PATH` if it #' exists. Note: some shells add `~/.local/bin` to `PATH` only if it exists at @@ -83,7 +85,6 @@ install_pkg_cli_apps <- function( lib.loc = NULL, overwrite = NA ) { - default_destdir <- is.null(destdir) destdir <- destdir %||% rapp_install_dir() dir.exists(destdir) || dir.create(destdir, recursive = TRUE) || @@ -92,7 +93,7 @@ install_pkg_cli_apps <- function( if (is_windows()) { ensure_path_windows(destdir) } else if (is_macos()) { - ensure_path_macos(destdir, default_destdir = default_destdir) + ensure_path_macos(destdir) } # existing Rapp launchers we're either overwriting or deleting @@ -522,14 +523,11 @@ get_env_win_registry <- function(name) { utils::readRegistry("Environment", hive = "HCU", view = "default")[[name]] } -ensure_path_macos <- function(destdir = rapp_install_dir(), default_destdir = TRUE) { +ensure_path_macos <- function(destdir = rapp_install_dir()) { if (Sys.getenv("RAPP_NO_MODIFY_PATH") != "") { return(FALSE) } stopifnot(is_macos()) - if (!isTRUE(default_destdir)) { - return(FALSE) - } destdir <- normalizePath(destdir, mustWork = TRUE) if (!identical(destdir, macos_default_install_dir())) { @@ -539,29 +537,25 @@ ensure_path_macos <- function(destdir = rapp_install_dir(), default_destdir = TR return(FALSE) } - zprofile <- path(path.expand("~"), ".zprofile") + zprofile <- macos_zprofile() + zprofile_display <- macos_zprofile_display(zprofile) lines <- macos_path_lines() if (profile_has_lines(zprofile, lines)) { warning( - "~/.local/bin PATH setup is already present in ~/.zprofile, ", + "~/.local/bin PATH setup is already present in ", + zprofile_display, + ", ", "but ~/.local/bin is still not on PATH.\n", "Restart your shell, or run:\n\n", - " source ~/.zprofile", + " source ", zprofile_display, call. = FALSE ) - } else { - cat( - paste0(c("", lines), collapse = "\n"), - "\n", - file = zprofile, - append = TRUE, - sep = "" - ) + } else if (write_macos_path_lines(zprofile, lines, zprofile_display)) { message( - "Added ~/.local/bin to PATH in ~/.zprofile.\n", + "Added ~/.local/bin to PATH in ", zprofile_display, ".\n", "Restart your shell, or run:\n\n", - " source ~/.zprofile" + " source ", zprofile_display ) } @@ -573,6 +567,22 @@ macos_default_install_dir <- function() { normalizePath(file.path(path.expand("~"), ".local", "bin"), mustWork = FALSE) } +macos_zprofile <- function() { + zdotdir <- Sys.getenv("ZDOTDIR") + if (!nzchar(zdotdir)) { + zdotdir <- path.expand("~") + } + path(zdotdir, ".zprofile") +} + +macos_zprofile_display <- function(zprofile) { + if (identical(path(zprofile), path(path.expand("~"), ".zprofile"))) { + "~/.zprofile" + } else { + zprofile + } +} + macos_path_lines <- function() { c( 'case ":$PATH:" in', @@ -586,10 +596,39 @@ profile_has_lines <- function(profile, lines) { if (!file.exists(profile)) { return(FALSE) } - profile <- paste(readLines(profile, warn = FALSE), collapse = "\n") + profile <- tryCatch( + paste(readLines(profile, warn = FALSE), collapse = "\n"), + error = function(e) "", + warning = function(w) "" + ) grepl(paste(lines, collapse = "\n"), profile, fixed = TRUE) } +write_macos_path_lines <- function(profile, lines, profile_display) { + tryCatch( + { + suppressWarnings(cat( + paste0(c("", lines), collapse = "\n"), + "\n", + file = profile, + append = TRUE, + sep = "" + )) + TRUE + }, + error = function(e) { + warning( + "Could not add ~/.local/bin to PATH in ", + profile_display, + ": ", + conditionMessage(e), + call. = FALSE + ) + FALSE + } + ) +} + path_has_dir <- function(dir, env_path = Sys.getenv("PATH")) { normalizePath(dir, mustWork = FALSE) %in% path_entries(env_path) } diff --git a/README.md b/README.md index 523d253..faaaf32 100644 --- a/README.md +++ b/README.md @@ -426,10 +426,12 @@ available location from `RAPP_BIN_DIR`, `XDG_BIN_HOME`, `XDG_DATA_HOME/../bin`, or the default location, `~/.local/bin` on macOS and Linux and `%LOCALAPPDATA%\Programs\R\Rapp\bin` on Windows. On Windows the directory is automatically added to `PATH`; on macOS, the -default `~/.local/bin` directory is added to `~/.zprofile` when needed. -On Linux the directory generally is already present on `PATH` (you may -need to log out and back in if the Rapp installer created the directory). -Use the `destdir` argument if you prefer an alternate location. +default `~/.local/bin` directory is added to `~/.zprofile` when needed +(`$ZDOTDIR/.zprofile` when `ZDOTDIR` is set). If that profile cannot be +updated, Rapp warns and continues installing launchers. On Linux the +directory generally is already present on `PATH` (you may need to log +out and back in if the Rapp installer created the directory). Use the +`destdir` argument if you prefer an alternate location. Use `#| launcher:` front matter to customize the installed launcher. For example, `name` changes the installed command name, and `vanilla`, diff --git a/man/install_pkg_cli_apps.Rd b/man/install_pkg_cli_apps.Rd index 152cf30..9b7c75a 100644 --- a/man/install_pkg_cli_apps.Rd +++ b/man/install_pkg_cli_apps.Rd @@ -73,8 +73,10 @@ If \code{destdir} is not provided, it is resolved in this order: On Windows, the resolved \code{destdir} is explicitly added to \code{PATH} (it generally is not by default). On macOS, when the default \verb{~/.local/bin} -destination is not already on \code{PATH}, it is added to \verb{~/.zprofile}. To -disable adding it to the \code{PATH}, set envvar \code{RAPP_NO_MODIFY_PATH=1}. +destination is not already on \code{PATH}, it is added to \verb{~/.zprofile}, or +\verb{$ZDOTDIR/.zprofile} when \code{ZDOTDIR} is set. If the profile cannot be +updated, a warning is emitted and installation continues. To disable adding it +to the \code{PATH}, set envvar \code{RAPP_NO_MODIFY_PATH=1}. On Linux, \verb{~/.local/bin} is typically already on \code{PATH} if it exists. Note: some shells add \verb{~/.local/bin} to \code{PATH} only if it exists at diff --git a/tests/testthat/test-install.R b/tests/testthat/test-install.R index 6bfa002..7268a85 100644 --- a/tests/testthat/test-install.R +++ b/tests/testthat/test-install.R @@ -266,6 +266,118 @@ test_that("install_pkg_cli_apps adds default macOS install dir to PATH setup", { }) +test_that("install_pkg_cli_apps adds explicit default macOS destdir to PATH setup", { + skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) + skip_on_cran() + + home <- tempfile("rapp-home-") + dir.create(home, recursive = TRUE) + withr::local_envvar( + HOME = home, + RAPP_NO_MODIFY_PATH = NA, + RAPP_BIN_DIR = NA, + XDG_BIN_HOME = NA, + XDG_DATA_HOME = NA, + PATH = "/usr/bin" + ) + on.exit(unlink(home, recursive = TRUE), add = TRUE) + destdir <- path(home, ".local", "bin") + + pkg <- "rappTestMacExplicitPath" + fake <- setup_fake_rapp_package( + tempdir(), + "-install-mac-explicit-path", + package = pkg + ) + on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE) + + app_path <- file.path(fake[["exec"]], "hello.R") + writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path) + + suppressMessages( + created <- install_pkg_cli_apps( + pkg, + destdir = "~/.local/bin", + lib.loc = fake[["lib"]] + ) + ) + + expect_same_path(created, path(destdir, "hello")) + expect_true(file.exists(file.path(home, ".zprofile"))) +}) + + +test_that("install_pkg_cli_apps respects ZDOTDIR for macOS PATH setup", { + skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) + skip_on_cran() + + home <- tempfile("rapp-home-") + zdotdir <- tempfile("rapp-zdotdir-") + dir.create(home, recursive = TRUE) + dir.create(zdotdir, recursive = TRUE) + withr::local_envvar( + HOME = home, + ZDOTDIR = zdotdir, + RAPP_NO_MODIFY_PATH = NA, + RAPP_BIN_DIR = NA, + XDG_BIN_HOME = NA, + XDG_DATA_HOME = NA, + PATH = "/usr/bin" + ) + on.exit(unlink(c(home, zdotdir), recursive = TRUE), add = TRUE) + + pkg <- "rappTestMacZdotdir" + fake <- setup_fake_rapp_package(tempdir(), "-install-mac-zdotdir", package = pkg) + on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE) + + app_path <- file.path(fake[["exec"]], "hello.R") + writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path) + + suppressMessages(install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]])) + + expect_false(file.exists(file.path(home, ".zprofile"))) + expect_true(file.exists(file.path(zdotdir, ".zprofile"))) +}) + + +test_that("install_pkg_cli_apps warns and continues when macOS PATH setup cannot be written", { + skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) + skip_on_cran() + + home <- tempfile("rapp-home-") + dir.create(home, recursive = TRUE) + dir.create(file.path(home, ".zprofile")) + withr::local_envvar( + HOME = home, + RAPP_NO_MODIFY_PATH = NA, + RAPP_BIN_DIR = NA, + XDG_BIN_HOME = NA, + XDG_DATA_HOME = NA, + PATH = "/usr/bin" + ) + on.exit(unlink(home, recursive = TRUE), add = TRUE) + + pkg <- "rappTestMacPathReadonly" + fake <- setup_fake_rapp_package( + tempdir(), + "-install-mac-path-readonly", + package = pkg + ) + on.exit(unlink(fake[["lib"]], recursive = TRUE), add = TRUE) + + app_path <- file.path(fake[["exec"]], "hello.R") + writeLines(c("#!/usr/bin/env Rapp", "print('hello')"), app_path) + + expect_warning( + suppressMessages( + created <- install_pkg_cli_apps(pkg, lib.loc = fake[["lib"]]) + ), + "Could not add ~/.local/bin to PATH" + ) + expect_same_path(created, path(home, ".local", "bin", "hello")) +}) + + test_that("install_pkg_cli_apps does not duplicate macOS PATH setup", { skip_if_not(identical(Sys.info()[["sysname"]], "Darwin")) skip_on_cran() From e9a1fcafb628c5c42a753ce95b4335ddeebe0e30 Mon Sep 17 00:00:00 2001 From: Tomasz Kalinowski Date: Tue, 9 Jun 2026 07:29:57 -0400 Subject: [PATCH 3/3] Regenerate documentation with roxygen --- DESCRIPTION | 2 +- man/install_pkg_cli_apps.Rd | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c37c3aa..d21c6a9 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -13,7 +13,6 @@ URL: https://github.com/r-lib/Rapp BugReports: https://github.com/r-lib/Rapp/issues Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.3 Suggests: testthat (>= 3.0.0), withr @@ -22,3 +21,4 @@ Config/testthat/parallel: true Config/testthat/start-first: help-snapshots, basics Imports: yaml12 +Config/roxygen2/version: 8.0.0 diff --git a/man/install_pkg_cli_apps.Rd b/man/install_pkg_cli_apps.Rd index 9b7c75a..7f7d5d4 100644 --- a/man/install_pkg_cli_apps.Rd +++ b/man/install_pkg_cli_apps.Rd @@ -75,8 +75,8 @@ On Windows, the resolved \code{destdir} is explicitly added to \code{PATH} (it generally is not by default). On macOS, when the default \verb{~/.local/bin} destination is not already on \code{PATH}, it is added to \verb{~/.zprofile}, or \verb{$ZDOTDIR/.zprofile} when \code{ZDOTDIR} is set. If the profile cannot be -updated, a warning is emitted and installation continues. To disable adding it -to the \code{PATH}, set envvar \code{RAPP_NO_MODIFY_PATH=1}. +updated, a warning is emitted and installation continues. To disable adding +it to the \code{PATH}, set envvar \code{RAPP_NO_MODIFY_PATH=1}. On Linux, \verb{~/.local/bin} is typically already on \code{PATH} if it exists. Note: some shells add \verb{~/.local/bin} to \code{PATH} only if it exists at