I'm developing (for the first time) an R package using Rcpp which implements an interface to another program (maxima). This package defines a C++ class, whose constructor needs to retrieve the path to an initialization script that gets installed with the package (the in-package path is inst/extdata/maxima-init.mac
. The path to this script is then used as a parameter to spawn a child process that runs the program.
In order to retrieve the path to the installed initialization script I'm calling the R function base::system.file
from within the C++ class constructor definition:
...
Environment env("package:base");
Function f = env["system.file"];
fs::path p(Rcpp::as<std::string>(f("extdata", "maxima-init.mac", Named("package") = "rmaxima")));
std::string utilsDir = p.parent_path().string();
...
# spawn child process using path in utilsDir
My R/zzz.R
creates an object of that class when the packages gets attached:
loadModule("Maxima", TRUE)
.onAttach <- function(libname, pkgname) {
"package:base" %in% search()
maxima <<- new(RMaxima)
}
I can install.packages(rmaxima)
and library(rmaxima)
just fine and the package works as expected.
I now want to increase my development efficiency by using devtools::load_all() to avoid having to R CMD build rmaxima
, install.packages(rmaxima)
and library(rmaxima)
each time I want to test changes. However, when calling devtools::load_all()
(or similarily devtools::test()
(working directory set to package root) my implementation freezes, because the variable utilsDir
is empty and therefore the process launching does not return (I guess it keeps waiting for a valid path). I eventually need to manually kill the process. The same thing happens without setting .onAttach()
Apparently devtools::load_all() does not resemble R's default search path on restart. What can I do? Is this the problem or am I missing something else?
I just came across the following notion of in the devtools::load_all()
R documentation file which could be a tip in the right direction
Shim files:
‘load_all’ also inserts shim functions into the imports environment of the loaded package. It presently adds a replacement version of ‘system.file’ which returns different paths from ‘base::system.file’. This is needed because installed and uninstalled package sources have different directory structures. Note that this is not a perfect replacement for base::system.file.
Also I realized, that devtools::load_all() only temporarily installs my package into, but somehow doesn't the files from my inst/
rcst@Velveeta:~$ ls -1R /tmp/RtmpdnvOQg/devtools_install_ee1e82c780/rmaxima/
/tmp/RtmpdnvOQg/devtools_install_ee1e82c780/rmaxima/:
DESCRIPTION
libs
Meta
NAMESPACE
/tmp/RtmpdnvOQg/devtools_install_ee1e82c780/rmaxima/libs:
rmaxima.so
/tmp/RtmpdnvOQg/devtools_install_ee1e82c780/rmaxima/Meta:
features.rds
package.rds
As it turns out devtools
provides a solution to exactly this problem.
In short: calling system.file
(i.e. from the global environitment and having the devtools package attached) solves the issue. Specifically the modification:
// Environment env("package:base");
// Function f = env["system.file"];
Function f("system.file");
fs::path p(Rcpp::as<std::string>(f("extdata", "maxima-init.mac", Named("package") = "rmaxima")));
std::string utilsDir = p.parent_path().string();
base::system.file(..., mustWork = FALSE)
returns an empty string if no match is found. devtools::load_all()
temporarily installs the packages inside /tmp/
(on my linux machine). The directory structure of the temporary installation differs from the one of the regular installation, i.e. the one created by install.packages()
. In my case, most notably, devtools::load_all()
does not copy the inst/
directory, which contains the initialization file.
Now, calling base::system.file("maxima-init.mac", package="rmaxima", mustWork=FALSE)
naturally fails, since it searches inside the temporary installation. Having devtools
attached masks system.file()
with devtools::system.file()
, which as mentioned above is "... meant to intercept calls to base::sysem.file()
" and behaves differently from base::system.file()
. Practically, I think this means, that it will search for the package's source directory instead of the temporary installation.
This way, simply calling system.file()
from the global environment calls the right function, either from devtools
or base
, for either the development or user version of the package automatically.
Nonetheless, using ccache
additionally (thanks @dirk) substantially speeds up my development workflow.