Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Get Rhome and libR from Preferences.jl when provided #496

Merged
merged 39 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
33b6674
Get Rhome and libR from preferences when provided
frankier Aug 4, 2023
902c5d6
Allow installing package without R installation
frankier Aug 4, 2023
92ffbab
Add docs for Preferences based R customization
frankier Aug 4, 2023
ce9756b
Only precompile when Rhome is set
frankier Sep 14, 2023
ed5ff4f
Add compat entry for Preferences
frankier Sep 14, 2023
75b836a
Add note about current downsides of installation time R configuration
frankier Sep 14, 2023
258be88
Update docs/src/installation.md
frankier Sep 15, 2023
229da88
Add additional note about switching to preference based Rlib config
frankier Sep 16, 2023
9da8881
Add _ option for R_HOME to explicitly unset it
frankier Oct 3, 2023
545cea1
Add docs for installing with R_HOME=_
frankier Oct 3, 2023
8040b3e
Add installation tests
frankier Oct 3, 2023
d6770a5
Fix typo in src/setup.jl
frankier Nov 14, 2023
10a0020
Update test/installation.jl
frankier Jan 3, 2024
a1eb561
Update test/runtests.jl
frankier Jan 3, 2024
a563f91
installation test improvements
palday Jan 6, 2024
b630644
argh loadpath
palday Jan 6, 2024
eb19da1
test debugging
palday Jan 6, 2024
a054681
reduce verbosity
palday Jan 6, 2024
1273c4b
wow Conda
palday Jan 6, 2024
5711d6b
try again
palday Jan 6, 2024
086c24c
try again
palday Jan 6, 2024
ced63bb
Remove @static Rhome/libR preference + force and document access
frankier Jan 7, 2024
2388328
Refer to old R installation method as R_HOME-based as shorthand rathe…
frankier Jan 8, 2024
4e686aa
Add cookbook snippet for usage with CondaPkg
frankier Jan 8, 2024
9e3fd9f
Clarify precompile-abort case and print explanitory message in this case
frankier Jan 8, 2024
f327698
Merge branch 'master' into preferences-r-installation
palday Jan 8, 2024
68f02e3
Merge branch 'master' of github.com:JuliaInterop/RCall.jl into pr/fra…
palday Jan 8, 2024
3e8e503
potential fix for mac CI
palday Jan 8, 2024
0a52d21
try something on windows
palday Jan 8, 2024
74b6120
patch bump
palday Jan 6, 2024
d86f4d4
Fix CondaPkg test to activate the environment
frankier Jan 21, 2024
6f47c4d
Refer to RCall by uuid in CondaPkg example
frankier Jan 21, 2024
5d89f03
Proof preferences installation docs a bit
frankier Jan 21, 2024
ac7ade3
Add note about use with CondaPkg and making sure the environment is a…
frankier Jan 21, 2024
bc86527
try forcing build on Windows
palday Jan 21, 2024
9d968d0
scope
palday Jan 21, 2024
c130c78
coverage
palday Jan 22, 2024
01aa738
Clarify the CondaPkg cookbook section
frankier Jan 30, 2024
1273105
Clarify the docs on R_HOME's special values
frankier Jan 30, 2024
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
8 changes: 6 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ha! Mixed feelings about this sneaky hack to pass coverage. The reason there is no coverage here is because when we shell out in installation.jl coverage stops being measured. It's probably not worth tracking it.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, it's definitely a hack but it's one I'm comfortable with for such "defense in depth" type measures.

"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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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)
Loading
Loading