Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions R/internal_utilities.R
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ get_libjvm_path <- function(java_home) {
recursive = TRUE,
full.names = TRUE
)
# Ignore AppleDouble files (._)
all_files <- all_files[!grepl("/\\._", all_files)]
if (length(all_files) > 0) lib_path <- all_files[1]
}
}
Expand Down Expand Up @@ -267,6 +269,34 @@ is_rjavaenv_cache_path <- function(path) {
return(startsWith(path_norm, cache_norm))
}

#' Resolve symlinks recursively on Unix systems
#'
#' @param path Character. Path to resolve.
#' @param max_depth Integer. Maximum symlink depth to follow (default 10).
#' @return Character. Resolved path (or original if not a symlink or on Windows).
#' @keywords internal
#' @noRd
resolve_symlinks <- function(path, max_depth = 10L) {
if (.Platform$OS.type != "unix" || !nzchar(path)) {
return(path)
}

real_path <- path
for (i in seq_len(max_depth)) {
link <- Sys.readlink(real_path)
if (is.na(link) || !nzchar(link)) {
break
}
# Handle relative symlinks
if (!startsWith(link, "/")) {
link <- file.path(dirname(real_path), link)
}
real_path <- link
}

return(real_path)
}

#' Internal readline wrapper for testability
#'
#' Wraps base::readline() to enable mocking in tests without polluting public API.
Expand Down Expand Up @@ -355,3 +385,23 @@ java_check_current_rjava_version <- function() {

return(major)
}

#' Find the actual extracted directory, ignoring hidden/metadata files
#'
#' @param temp_dir The directory where files were extracted.
#' @return The path to the first non-hidden directory found.
#' @keywords internal
._find_extracted_dir <- function(temp_dir) {
# Ignore hidden files like .DS_Store or AppleDouble files (._)
all_files <- list.files(temp_dir, full.names = TRUE)
extracted_dirs <- all_files[
dir.exists(all_files) & !grepl("^\\._", basename(all_files))
]

if (length(extracted_dirs) == 0) {
cli::cli_abort(
"No directory found after unpacking the Java distribution at {.path {temp_dir}}"
)
}
return(extracted_dirs[1])
}
136 changes: 108 additions & 28 deletions R/java_env.R
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,8 @@ java_env_set_rprofile <- function(
project_path <- ifelse(is.null(project_path), getwd(), project_path)
rprofile_path <- file.path(project_path, ".Rprofile")

# Normalize the path for Windows
if (.Platform$OS.type == "windows") {
java_home <- gsub("\\\\", "/", java_home)
}
# Normalize the path for consistency (especially on Windows)
java_home <- normalizePath(java_home, winslash = "/", mustWork = FALSE)

lines_to_add <- c(
"# rJavaEnv begin: Manage JAVA_HOME",
Expand Down Expand Up @@ -273,10 +271,11 @@ java_check_version_rjava <- function(
}

# Get check result (either cached or fresh)
cache_key <- Sys.getenv("JAVA_HOME")
# Use the effective java_home as cache key (what we're actually checking)
effective_java_home <- java_home

if (.use_cache) {
data <- ._java_version_check_rjava_impl(java_home, cache_key)
data <- ._java_version_check_rjava_impl(java_home, effective_java_home)
} else {
# Bypass cache - call the implementation directly
data <- ._java_version_check_rjava_impl_original(java_home)
Expand Down Expand Up @@ -400,10 +399,15 @@ java_check_version_cmd <- function(
.use_cache = FALSE
) {
# Get data (either cached or fresh)
cache_key <- Sys.getenv("JAVA_HOME")
# Use the effective java_home as cache key
effective_java_home <- if (is.null(java_home)) {
Sys.getenv("JAVA_HOME")
} else {
java_home
}

if (.use_cache) {
data <- ._java_version_check_impl(java_home, cache_key)
data <- ._java_version_check_impl(java_home, effective_java_home)
} else {
# Bypass cache - call the implementation directly
data <- ._java_version_check_impl_original(java_home)
Expand Down Expand Up @@ -451,24 +455,67 @@ java_check_version_cmd <- function(
return(FALSE)
}

# Check if java executable exists in the PATH
if (!nzchar(Sys.which("java"))) {
if (!is.null(java_home)) {
Sys.setenv(JAVA_HOME = old_java_home)
Sys.setenv(PATH = old_path)
# Get Java path - use explicit path if java_home is specified
if (!is.null(java_home)) {
# Direct check of the executable within the provided java_home
# This avoids Sys.which potentially picking up system shims (common on macOS)
# or other executables in the PATH

# Determine executable name based on OS
exe_name <- if (.Platform$OS.type == "windows") "java.exe" else "java"
java_bin_candidate <- file.path(java_home, "bin", exe_name)

if (file.exists(java_bin_candidate)) {
java_bin <- java_bin_candidate
} else {
# Fallback to failing if not found in the expected location
return(FALSE)
}
} else {
java_bin <- Sys.which("java")
if (!nzchar(java_bin)) {
return(FALSE)
}
return(FALSE)
}
which_java <- java_bin

# On macOS, the 'java' launcher stub dynamically loads libjvm.dylib.
# If another JDK (e.g., system Temurin) is in the default library search path,
# the launcher may load the wrong JVM library even if JAVA_HOME is set.
# We fix this by targeting DYLD_LIBRARY_PATH specifically for this subprocess.
# Note: We do NOT set DYLD_LIBRARY_PATH globally in java_env_set() because:
# 1. rJava's .jinit() loads libjvm.dylib directly from path, bypassing the stub.
# 2. Global DYLD_* vars are restricted by macOS SIP and can cause side effects.
if (Sys.info()[["sysname"]] == "Darwin" && !is.null(java_home)) {
libjvm_path <- get_libjvm_path(java_home)
if (!is.null(libjvm_path)) {
lib_server <- dirname(libjvm_path)
old_dyld_path <- Sys.getenv("DYLD_LIBRARY_PATH", unset = NA)
if (is.na(old_dyld_path)) {
on.exit(Sys.unsetenv("DYLD_LIBRARY_PATH"), add = TRUE)
} else {
on.exit(Sys.setenv(DYLD_LIBRARY_PATH = old_dyld_path), add = TRUE)
}
Sys.setenv(DYLD_LIBRARY_PATH = lib_server)
}

# Also temporarily set JAVA_HOME for this check
saved_java_home_for_darwin <- Sys.getenv("JAVA_HOME", unset = NA)
if (is.na(saved_java_home_for_darwin)) {
on.exit(Sys.unsetenv("JAVA_HOME"), add = TRUE)
} else {
on.exit(Sys.setenv(JAVA_HOME = saved_java_home_for_darwin), add = TRUE)
}
Sys.setenv(JAVA_HOME = java_home)
}

# Get Java path and version info (without printing)
which_java <- Sys.which("java")
java_ver <- tryCatch(
system2(
"java",
java_bin,
args = "-version",
stdout = TRUE,
stderr = TRUE,
timeout = 10
timeout = 30
),
error = function(e) NULL
)
Expand Down Expand Up @@ -496,17 +543,15 @@ java_check_version_cmd <- function(
return(FALSE)
}

# Extract Java version
java_ver_string <- java_ver[[1]]
matches <- regexec(
'(openjdk|java) (version )?(\\\")?([0-9]{1,2})',
java_ver_string
)
major_java_ver <- regmatches(java_ver_string, matches)[[1]][5]
major_java_ver <- ._java_parse_version_output(java_ver)

# Fix 1 to 8, as Java 8 prints "1.8"
if (major_java_ver == "1") {
major_java_ver <- "8"
if (isFALSE(major_java_ver)) {
# Restore original environment
if (!is.null(java_home)) {
Sys.setenv(JAVA_HOME = old_java_home)
Sys.setenv(PATH = old_path)
}
return(FALSE)
}

# Restore original JAVA_HOME and PATH
Expand Down Expand Up @@ -589,3 +634,38 @@ java_get_home <- function() {
}
java_home
}

#' Internal helper to parse java version output
#' @noRd
._java_parse_version_output <- function(java_ver) {
# Handle null/empty input
if (is.null(java_ver) || length(java_ver) == 0) {
return(FALSE)
}

# Extract Java version
# Iterate over all lines to find the version string
# This is needed because sometimes there is noise (e.g. "Picked up ...")
major_java_ver <- NULL
for (line in java_ver) {
matches <- regexec(
'^[[:space:]]*(openjdk|java)[[:space:]]+(version[[:space:]]+)?(")?([0-9]+)',
line
)
if (matches[[1]][1] != -1) {
major_java_ver <- regmatches(line, matches)[[1]][5]
break
}
}

if (is.null(major_java_ver)) {
return(FALSE)
}

# Fix 1 to 8, as Java 8 prints "1.8"
if (major_java_ver == "1") {
major_java_ver <- "8"
}

return(major_java_ver)
}
43 changes: 8 additions & 35 deletions R/java_find.R
Original file line number Diff line number Diff line change
Expand Up @@ -31,20 +31,7 @@
# 2. PATH Lookup (Resolving Symlinks)
java_bin <- Sys.which("java")
if (nzchar(java_bin)) {
real_path <- java_bin
# Recursive symlink resolution for Linux (handles /etc/alternatives)
if (.Platform$OS.type == "unix") {
for (i in 1:10) {
link <- Sys.readlink(real_path)
if (!nzchar(link)) {
break
}
if (!startsWith(link, "/")) {
link <- file.path(dirname(real_path), link)
}
real_path <- link
}
}
real_path <- resolve_symlinks(java_bin)
# If path ends in /bin/java, the grandparent dir is JAVA_HOME
if (grepl("[/\\\\]bin[/\\\\]java(\\.exe)?$", real_path)) {
candidates <- c(candidates, dirname(dirname(real_path)))
Expand Down Expand Up @@ -143,6 +130,12 @@
# Filter out rJavaEnv cache paths - we only want true system installations
candidates <- Filter(function(x) !is_rjavaenv_cache_path(x), candidates)

# macOS-specific: Filter out /usr to avoid system stubs (like /usr/bin/java)
# which can cause timeouts and aren't real JDK homes
if (os == "macos") {
candidates <- Filter(function(x) x != "/usr", candidates)
}

# Must contain bin/java to be valid - this prevents returning corrupted/empty JDK folders
valid_homes <- Filter(
function(x) {
Expand Down Expand Up @@ -197,13 +190,6 @@
# Set is_default to FALSE - it will be calculated by the caller
result_df$is_default <- FALSE

# 7. Sort by version (descending only, since is_default is all FALSE)
sort_order <- order(
-as.numeric(result_df$version),
decreasing = TRUE
)
result_df <- result_df[sort_order, ]

rownames(result_df) <- NULL
return(result_df)
}
Expand Down Expand Up @@ -321,20 +307,7 @@ java_find_system <- function(quiet = TRUE, .use_cache = FALSE) {
if (is.null(default_java)) {
java_bin <- Sys.which("java")
if (nzchar(java_bin)) {
real_path <- java_bin
# Recursive symlink resolution
if (.Platform$OS.type == "unix") {
for (i in 1:10) {
link <- Sys.readlink(real_path)
if (!nzchar(link)) {
break
}
if (!startsWith(link, "/")) {
link <- file.path(dirname(real_path), link)
}
real_path <- link
}
}
real_path <- resolve_symlinks(java_bin)
# Extract JAVA_HOME from /bin/java path
if (grepl("[/\\\\]bin[/\\\\]java(\\.exe)?$", real_path)) {
default_java <- normalizePath(
Expand Down
7 changes: 6 additions & 1 deletion R/java_unpack.R
Original file line number Diff line number Diff line change
Expand Up @@ -97,9 +97,14 @@ java_unpack <- function(
}

# Safely find the extracted directory
extracted_root_dir <- list.files(temp_dir, full.names = TRUE)[1]
extracted_root_dir <- ._find_extracted_dir(temp_dir)

if (platform == "macos") {
extracted_dir <- file.path(extracted_root_dir, "Contents", "Home")
# Some distributions might not have Contents/Home if they were prepared differently
if (!dir.exists(extracted_dir)) {
extracted_dir <- extracted_root_dir
}
} else {
extracted_dir <- extracted_root_dir
}
Expand Down
22 changes: 19 additions & 3 deletions _pkgdown.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ navbar:
structure:
left: [intro, reference, articles, tutorials, news]
right: [search, github, lightswitch]
components:
articles:
text: Articles
menu:
- text: "Getting Started"
- text: "Step-by-step Java Setup"
href: articles/rJavaEnv-step-by-step.html
- text: "Multiple Java with targets & callr"
href: articles/multiple-java-with-targets-callr.html
- text: "---"
- text: "About"
- text: "Why rJavaEnv?"
href: articles/why-rJavaEnv.html
- text: "---"
- text: "Advanced"
- text: "Install rJava from Source"
href: articles/install-rjava-from-source.html
- text: "Using rJavaEnv in Packages"
href: articles/for-developers.html

home:
title: 'rJavaEnv: `Java` Environments for R Projects'
Expand Down Expand Up @@ -99,16 +118,13 @@ reference:

articles:
- title: "Getting Started"
navbar: "Get started"
contents:
- rJavaEnv-step-by-step
- multiple-java-with-targets-callr
- title: "About rJavaEnv"
navbar: "About"
contents:
- why-rJavaEnv
- title: "Advanced Topics"
navbar: "Advanced"
contents:
- install-rjava-from-source
- for-developers
Loading
Loading