Skip to content

Commit

Permalink
Get Rhome and libR from Preferences.jl when provided (#496)
Browse files Browse the repository at this point in the history
* Get Rhome and libR from preferences when provided

* Allow installing package without R installation

This is in case the user wants to specify and installation via
Preferences.jl after installation. An error message is printed at import
time if no installation is available.

* Add docs for Preferences based R customization

* Only precompile when Rhome is set

* Add note about current downsides of installation time R configuration

* Add additional note about switching to preference based Rlib config

* Add _ option for R_HOME to explicitly unset it

* Add docs for installing with R_HOME=_

* Add installation tests

* Add cookbook snippet for usage with CondaPkg

* Clarify precompile-abort case and print explanitory message in this case

* patch bump

* Refer to RCall by uuid in CondaPkg example

* Add note about use with CondaPkg and making sure the environment is activate

* coverage

---------

Co-authored-by: Phillip Alday <me@phillipalday.com>
  • Loading branch information
frankier and palday committed Jan 30, 2024
1 parent 5d0ddeb commit 31e7859
Show file tree
Hide file tree
Showing 12 changed files with 357 additions and 22 deletions.
8 changes: 6 additions & 2 deletions Project.toml
@@ -1,7 +1,7 @@
name = "RCall"
uuid = "6f49c342-dc21-5d91-9882-a32aef131414"
authors = ["Douglas Bates <dmbates@gmail.com>", "Randy Lai <randy.cs.lai@gmail.com>", "Simon Byrne <simonbyrne@gmail.com>"]
version = "0.14.0"
version = "0.14.1"

[deps]
CategoricalArrays = "324d7699-5711-5eae-9e2f-1d82baa6b597"
Expand All @@ -11,6 +11,7 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
Missings = "e1d29d7a-bbdc-5cf2-9ac0-f12de2c33e28"
Preferences = "21216c6a-2e73-6563-6e65-726566657250"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
Expand All @@ -24,17 +25,20 @@ Conda = "1.4"
DataFrames = "0.21, 0.22, 1.0"
DataStructures = "0.5, 0.6, 0.7, 0.8, 0.9, 0.10, 0.11, 0.12, 0.13, 0.14, 0.15, 0.16, 0.17, 0.18"
Missings = "0.2, 0.3, 0.4, 1.0"
Preferences = "1"
Requires = "0.5.2, 1"
StatsModels = "0.6, 0.7"
WinReg = "0.2, 0.3, 1"
julia = "1"

[extras]
AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9"
CondaPkg = "992eb4ea-22a4-4c89-a5bb-47a3300528ab"
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
REPL = "3fa0cd96-eef1-5676-8a61-b3b8758bbffb"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Dates", "AxisArrays", "REPL", "Test", "Random"]
test = ["Dates", "AxisArrays", "REPL", "Test", "Random", "CondaPkg", "Pkg"]
39 changes: 28 additions & 11 deletions deps/build.jl
Expand Up @@ -25,6 +25,7 @@ try
@info "Using previously configured R at $Rhome with libR in $libR."
else
Rhome = get(ENV, "R_HOME", "")
libR = nothing
if Rhome == "*"
# install with Conda
@info "Installing R via Conda. To use a different R installation,"*
Expand All @@ -35,6 +36,8 @@ try
Conda.add("r-base>=3.4.0,<5") # greater than or equal to 3.4.0 AND strictly less than 5.0
Rhome = joinpath(Conda.LIBDIR, "R")
libR = locate_libR(Rhome)
elseif Rhome == "_"
Rhome = ""
else
if isempty(Rhome)
try Rhome = readchomp(`R RHOME`); catch; end
Expand All @@ -48,22 +51,36 @@ try
try Rhome = WinReg.querykey(WinReg.HKEY_CURRENT_USER,
"Software\\R-Core\\R", "InstallPath"); catch; end
end
else
if !isdir(Rhome)
error("R_HOME is not a directory.")
end
end

isempty(Rhome) && error("R cannot be found. Set the \"R_HOME\" environment variable to re-run Pkg.build(\"RCall\").")
libR = locate_libR(Rhome)
if !isempty(Rhome) && !isdir(Rhome)
error("R_HOME is not a directory.")
end

if !isempty(Rhome)
libR = locate_libR(Rhome)
end
end

@info "Using R at $Rhome and libR at $libR."
if DepFile.Rhome != Rhome || DepFile.libR != libR
if isempty(Rhome)
@info (
"No R installation found. " *
"You will not be able to import RCall without " *
"providing values for its preferences Rhome and libR."
)
open(depfile, "w") do f
println(f, "const Rhome = \"", escape_string(Rhome), '"')
println(f, "const libR = \"", escape_string(libR), '"')
println(f, "const conda_provided_r = $(conda_provided_r)")
println(f, "const Rhome = \"\"")
println(f, "const libR = \"\"")
println(f, "const conda_provided_r = nothing")
end
else
@info "Using R at $Rhome and libR at $libR."
if DepFile.Rhome != Rhome || DepFile.libR != libR
open(depfile, "w") do f
println(f, "const Rhome = \"", escape_string(Rhome), '"')
println(f, "const libR = \"", escape_string(libR), '"')
println(f, "const conda_provided_r = $(conda_provided_r)")
end
end
end
end
Expand Down
73 changes: 69 additions & 4 deletions docs/src/installation.md
Expand Up @@ -8,7 +8,65 @@ Pkg.add("RCall")

## Customizing the R installation

The RCall build script (run by `Pkg.add`)
There are two ways to configure the R installation used by RCall.jl:

* [Using Julia's Preferences system](#Customizing-the-R-installation-using-Julia's-Preferences-system)
* [At RCall.jl install time, or when manually re-building RCall.jl, using the `R_HOME` environment variable](#Customizing-the-R-installation-using-R_HOME)

Should you experience problems with any of these methods, please [open an issue](https://github.com/JuliaStats/RCall.jl/issues/new).

### Customizing the R installation using Julia's Preferences system

You can customize the R installation using [Julia's Preferences system](https://docs.julialang.org/en/v1/manual/code-loading/#preferences) by providing appropriate paths using RCall's `Rhome` and `libR` preferences. Julia's Preferences system allows these to be set in a few different ways. One possibility is to add the following to a `LocalPreferences.toml` file in the same directory as a project's `Project.toml` file:

```toml
[RCall]
Rhome = "/path/to/env/lib/R"
libR = "/path/to/env/lib/R/lib/libR.so"
```

!!! note
When these preferences are set, they take precedence over the R installation configured using the `R_HOME` environment variable when RCall.jl was last built.

#### (Experimental) Usage with CondaPkg

Unlike [customizing the R installation using `R_HOME`](#Customizing-the-R-installation-using-R_HOME), the Preferences-based approach allows for each of your Julia projects using RCall.jl to use a different R installation. As such, it is appropriate for when you want to install and manage R with [CondaPkg](https://github.com/JuliaPy/CondaPkg.jl). Assuming that RCall and CondaPkg are installed, the following script will install a CondaPkg-managed R and set the correct preferences so that RCall.jl will make use of it.

```
using Libdl
using CondaPkg
using Preferences
using UUIDs
const RCALL_UUID = UUID("6f49c342-dc21-5d91-9882-a32aef131414")
CondaPkg.add("r")
target_rhome = joinpath(CondaPkg.envdir(), "lib", "R")
if Sys.iswindows()
target_libr = joinpath(Rhome, "bin", Sys.WORD_SIZE==64 ? "x64" : "i386", "R.dll")
else
target_libr = joinpath(Rhome, "lib", "libR.$(Libdl.dlext)")
end
set_preferences!(RCALL_UUID, "Rhome" => target_rhome, "libR" => target_libr)
```

So that CondaPkg managed R finds the correct versions of its shared library dependencies, such as BLAS, you must arrange for the Conda environment to be active when `RCall` is imported so that native library loading paths are set up correctly. If you do not do so, it is still possible that things will appear to work correctly if compatible versions are available from elsewhere in your library loading path, but the resulting code can break in some environments and so is not portable.

At the moment there are two options for arranging for this:
1. (Recommended) Use `CondaPkg.activate!(ENV)` to permanently modify the environment *before* loading RCall.
2. (Experimental) Use `CondaPkg.withenv()` to change the environment while loading RCall/R and R libraries using native code. After the `CondaPkg.withenv()` block, the Conda environment will no longer be active. This approach may be needed if you need to return to a unmodified environment after loading R. Note this approach has not been thouroughly tested and may not work with all R packages.

```julia
RCall = CondaPkg.withenv() do
RCall = @eval using RCall
# Load all R libraries that may load native code from the Conda environment here
return RCall
end
```

### Customizing the R installation using `R_HOME`

The RCall build script (run by `Pkg.add(...)` or `Pkg.build(...)`)
will check for an existing R installation by looking in the following locations,
in order.

Expand All @@ -19,17 +77,24 @@ in order.
* Otherwise, on Windows, it looks in the [Windows registry](https://cran.r-project.org/bin/windows/base/rw-FAQ.html#Does-R-use-the-Registry_003f).

To change which R installation is used for RCall, set the `R_HOME` environment variable
and run `Pkg.build("RCall")`. Once this is configured, RCall remembers the location
and run `Pkg.build("RCall")`. Once this is configured, RCall remembers the location
of R in future updates, so you don't need to set `R_HOME` permanently.

```julia
ENV["R_HOME"] = "....directory of R home...."
Pkg.build("RCall")
```

When `R_HOME` is set to `"*"`, RCall.jl will automatically install R for you using [Conda](https://github.com/JuliaPy/Conda.jl).
As well as being setting `R_HOME` to a path, it can also be set to certain special values:

Should you experience problems with any of these methods, please [open an issue](https://github.com/JuliaStats/RCall.jl/issues/new).
* When `R_HOME="*"`, RCall.jl will automatically install R for you using [Conda](https://github.com/JuliaPy/Conda.jl).
* When `R_HOME=""`, or is unset, RCall will try to locate `R_HOME` by asking the copy of R in your `PATH` and then --- on Windows only --- by checking the registry.
* When `R_HOME="_"`, you opt out of all attempts to automatically locate R.

In case no R installation is found or given at build time, the build will complete with a warning, but no error. RCall.jl will not be importable until you set a location for R [using the Preferences system](#Customizing-the-R-installation-using-Julia's-Preferences-system).

!!! note "R_HOME-based R installation is shared"
When the R installation is configured at RCall.jl install time, the absolute path to the R installation is currently hard-coded into the RCall.jl package, which can be shared between projects. This may cause problems if you are using different R installations for different projects which end up using the same copy of RCall.jl. In this case, please [use the Preferences system instead](#Customizing-the-R-installation-using-Julia's-Preferences-system) which keeps different copies of the compiled RCall for different R installations. You do not need to rebuild RCall.jl manually for this, simply setting the relevant preferences will trigger rebuilds as necessary.

## Standard installations

Expand Down
33 changes: 28 additions & 5 deletions src/RCall.jl
@@ -1,6 +1,6 @@
__precompile__()
module RCall

using Preferences
using Requires
using Dates
using Libdl
Expand Down Expand Up @@ -29,11 +29,34 @@ export RObject,
robject, rcopy, rparse, rprint, reval, rcall, rlang,
rimport, @rimport, @rlibrary, @rput, @rget, @var_str, @R_str

const depfile = joinpath(dirname(@__FILE__),"..","deps","deps.jl")
if isfile(depfile)
include(depfile)
# These two preference get marked as compile-time preferences by being accessed
# here
const Rhome_set_as_preference = @has_preference("Rhome")
const libR_set_as_preference = @has_preference("libR")

if Rhome_set_as_preference || libR_set_as_preference
if !(Rhome_set_as_preference && libR_set_as_preference)
error("RCall: Either both Rhome and libR must be set or neither of them")
end
const Rhome = @load_preference("Rhome")
const libR = @load_preference("libR")
const conda_provided_r = false
else
error("RCall not properly installed. Please run Pkg.build(\"RCall\")")
const depfile = joinpath(dirname(@__FILE__),"..","deps","deps.jl")
if isfile(depfile)
include(depfile)
else
error("RCall not properly installed. Please run Pkg.build(\"RCall\")")
end
end

if Rhome == ""
@info (
"No R installation found by RCall.jl. " *
"Precompilation of RCall and all dependent packages postponed. " *
"Importing RCall will fail until an R installation is configured beforehand."
)
__precompile__(false)
end

include("types.jl")
Expand Down
6 changes: 6 additions & 0 deletions src/setup.jl
Expand Up @@ -171,6 +171,12 @@ end
include(joinpath(dirname(@__FILE__),"..","deps","setup.jl"))

function __init__()
# This should actually error much sooner, but this is just in case
isempty(Rhome) && error(
"No R installation was detected at RCall installation time. " *
"Please provided the location of R by setting the Rhome and libR preferences or " *
"else set R_HOME='*' and rerun Pkg.build(\"RCall\") to use Conda.jl.")

validate_libR(libR)

# Check if R already running
Expand Down
70 changes: 70 additions & 0 deletions test/installation.jl
@@ -0,0 +1,70 @@
# This file is used to test installation of the RCall package. We run
# a new Julia process in a temporary environment so that we
# can test what happens without already having imported RCall.

using Test

const RCALL_DIR = dirname(@__DIR__)

function test_installation(file, project=mktempdir())
path = joinpath(@__DIR__, "installation", file)
@static if Sys.isunix()
# this weird stub is necessary so that all the nested conda installation processes
# have access to the PATH
cmd = `sh -c $(Base.julia_cmd()) --project=$(project) $(path)`
elseif Sys.iswindows()
cmd = `cmd /C $(Base.julia_cmd()) --project=$(project) $(path)`
else
error("What system are you on?!")
end
cmd = Cmd(cmd; env=Dict("RCALL_DIR" => RCALL_DIR))
@test mktemp() do file, io
try
result = run(pipeline(cmd; stdout=io, stderr=io))
return success(result)
catch
@error open(f -> read(f, String), file)
return false
end
end
end

mktempdir() do dir
@testset "No R" begin
test_installation("rcall_without_r.jl", dir)
end
# We want to guard this with a version check so we don't run into the following
# (non-widespread) issue on older versions of Julia:
# https://github.com/JuliaLang/julia/issues/34276
# (related to incompatible libstdc++ versions)
@static if VERSION v"1.9"
@testset "Preferences" begin
test_installation("swap_to_prefs_and_condapkg.jl", dir)
end
end
end

# We want to guard this with a version check so we don't run into the following
# issue on older versions of Julia:
# https://github.com/JuliaLang/julia/issues/34276
# (related to incompatible libstdc++ versions)
@static if VERSION v"1.9"
# Test whether we can install RCall with Conda, and then switch to using
# Preferences + CondaPkg
mktempdir() do dir
# we run into weird issues with this on CI
@static if Sys.isunix()
@testset "Conda" begin
test_installation("install_conda.jl", dir)
end
end
@testset "Swap to Preferences" begin
test_installation("swap_to_prefs_and_condapkg.jl", dir)
end
@static if Sys.isunix()
@testset "Swap back from Preferences" begin
test_installation("drop_preferences.jl", dir)
end
end
end
end
18 changes: 18 additions & 0 deletions test/installation/drop_preferences.jl
@@ -0,0 +1,18 @@
# Test removal of Rhome from preferences.
#
# If run after `install_conda.jl` and `swap_to_prefs_and_condapkg.jl` in the same enviroment,
# then it tests returning to the build status quo after removal of preferences.
#
# This file is meant to be run in an embedded process spawned by installation.jl.
@debug ENV["RCALL_DIR"]
using Preferences, UUIDs

set_preferences!(UUID("6f49c342-dc21-5d91-9882-a32aef131414"),
"Rhome" => nothing, "libR" => nothing; force=true)

RCall = Base.require(Main, :RCall)
if occursin(r"/conda/3/([^/]+/)?lib/R", RCall.Rhome)
exit(0)
end
println(stderr, "Wrong Conda Rhome $(rcall.Rhome)")
exit(1)
18 changes: 18 additions & 0 deletions test/installation/install_conda.jl
@@ -0,0 +1,18 @@
# Test installation of RCall when R is not present on the system and R_HOME="*",
# which leads to the autoinstallation of Conda.jl and R via Conda.jl
#
# This file is meant to be run in an embedded process spawned by installation.jl.
@debug ENV["RCALL_DIR"]

using Pkg

ENV["R_HOME"] = "*"
Pkg.add(;path=ENV["RCALL_DIR"])
Pkg.build("RCall")

RCall = Base.require(Main, :RCall)
if occursin(r"/conda/3/([^/]+/)?lib/R", RCall.Rhome)
exit(0)
end
println(stderr, "Wrong Conda Rhome $(rcall.Rhome)")
exit(1)

2 comments on commit 31e7859

@palday
Copy link
Collaborator

@palday palday commented on 31e7859 Jan 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/99890

Tip: Release Notes

Did you know you can add release notes too? Just add markdown formatted text underneath the comment after the text
"Release notes:" and it will be added to the registry PR, and if TagBot is installed it will also be added to the
release that TagBot creates. i.e.

@JuliaRegistrator register

Release notes:

## Breaking changes

- blah

To add them here just re-invoke and the PR will be updated.

Tagging

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.14.1 -m "<description of version>" 31e7859ab2757d8019b42eb18522703415ff8c85
git push origin v0.14.1

Please sign in to comment.