1 /*
2 * RVersionsPosix.cpp
3 *
4 * Copyright (C) 2021 by RStudio, PBC
5 *
6 * Unless you have received this program directly from RStudio pursuant
7 * to the terms of a commercial license agreement with RStudio, then
8 * this program is licensed to you under the terms of version 3 of the
9 * GNU Affero General Public License. This program is distributed WITHOUT
10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13 *
14 */
15
16 #include <core/r_util/RVersionsPosix.hpp>
17
18 #include <iostream>
19 #include <algorithm>
20
21 #include <boost/bind/bind.hpp>
22
23 #include <core/Algorithm.hpp>
24 #include <core/FileSerializer.hpp>
25 #include <core/r_util/REnvironment.hpp>
26 #include <core/json/JsonRpc.hpp>
27
28 #include <core/system/Environment.hpp>
29 #include <core/system/System.hpp>
30
31 #ifdef __APPLE__
32 #define kRFrameworkVersions "/Library/Frameworks/R.framework/Versions"
33 #define kRScriptPath "Resources/bin/R"
34 #endif
35
36 using namespace boost::placeholders;
37
38 namespace rstudio {
39 namespace core {
40 namespace r_util {
41
42 namespace {
43
realPaths(const std::vector<FilePath> & paths)44 std::vector<FilePath> realPaths(const std::vector<FilePath>& paths)
45 {
46 std::vector<FilePath> realPaths;
47 for (const FilePath& path : paths)
48 {
49 FilePath realPath;
50 Error error = core::system::realPath(path.getAbsolutePath(), &realPath);
51 if (!error)
52 realPaths.push_back(realPath);
53 else
54 LOG_ERROR(error);
55 }
56 return realPaths;
57 }
58
removeNonExistent(const std::vector<FilePath> & paths)59 std::vector<FilePath> removeNonExistent(const std::vector<FilePath>& paths)
60 {
61 std::vector<FilePath> filteredPaths;
62 for (const FilePath& path : paths)
63 {
64 if (path.exists())
65 filteredPaths.push_back(path);
66 }
67 return filteredPaths;
68 }
69
scanForRHomePaths(const core::FilePath & rootDir,std::vector<FilePath> * pHomePaths)70 void scanForRHomePaths(const core::FilePath& rootDir,
71 std::vector<FilePath>* pHomePaths)
72 {
73 if (rootDir.exists())
74 {
75 std::vector<FilePath> rDirs;
76 Error error = rootDir.getChildren(rDirs);
77 if (error)
78 LOG_ERROR(error);
79 for (const FilePath& rDir : rDirs)
80 {
81 if (rDir.completeChildPath("bin/R").exists())
82 pHomePaths->push_back(rDir);
83 }
84 }
85 }
86
87
88 } // anonymous namespace
89
operator <<(std::ostream & os,const RVersion & version)90 std::ostream& operator<<(std::ostream& os, const RVersion& version)
91 {
92 os << version.number();
93
94 if (!version.label().empty())
95 os << version.label() << std::endl;
96
97 os << std::endl;
98 os << version.homeDir() << std::endl;
99 for (const core::system::Option& option : version.environment())
100 {
101 os << option.first << "=" << option.second << std::endl;
102 }
103 os << std::endl;
104
105 return os;
106 }
107
enumerateRVersions(std::vector<FilePath> rHomePaths,std::vector<r_util::RVersion> rEntries,bool scanForOtherVersions,const FilePath & ldPathsScript,const std::string & ldLibraryPath,const FilePath & modulesBinaryPath)108 std::vector<RVersion> enumerateRVersions(
109 std::vector<FilePath> rHomePaths,
110 std::vector<r_util::RVersion> rEntries,
111 bool scanForOtherVersions,
112 const FilePath& ldPathsScript,
113 const std::string& ldLibraryPath,
114 const FilePath& modulesBinaryPath)
115 {
116 std::vector<RVersion> rVersions;
117
118 // scan if requested
119 if (scanForOtherVersions)
120 {
121 // start with all of the typical script locations
122 //rHomePaths.push_back(FilePath("/usr/lib/R"));
123 //rHomePaths.push_back(FilePath("/usr/lib64/R"));
124 rHomePaths.push_back(FilePath("/usr/local/lib/R"));
125 //rHomePaths.push_back(FilePath("/usr/local/lib64/R"));
126 //rHomePaths.push_back(FilePath("/opt/local/lib/R"));
127 //rHomePaths.push_back(FilePath("/opt/local/lib64/R"));
128
129 // scan /opt/R and /opt/local/R
130 scanForRHomePaths(FilePath("/opt/R"), &rHomePaths);
131 scanForRHomePaths(FilePath("/opt/local/R"), &rHomePaths);
132 }
133
134 // filter on existence, capture real paths, and eliminate duplicates
135 rHomePaths = removeNonExistent(rHomePaths);
136 rHomePaths = realPaths(rHomePaths);
137 std::sort(rHomePaths.begin(), rHomePaths.end());
138 rHomePaths.erase(std::unique(rHomePaths.begin(), rHomePaths.end()),
139 rHomePaths.end());
140
141 // resolve user defined r entries first
142 // when duplicates are removed, the default paths
143 // that are equivalent to the user defined entries (but which contain less metadata) will be removed
144 for (r_util::RVersion& rEntry : rEntries)
145 {
146 // compute R script path
147 FilePath rScriptPath = rEntry.homeDir().completeChildPath("bin/R");
148 if (!rScriptPath.exists())
149 {
150 if (rEntry.module().empty())
151 {
152 LOG_ERROR_MESSAGE("Invalid R version specified - path does not exist: " +
153 rScriptPath.getAbsolutePath() + " - version will be skipped");
154 continue;
155 }
156 else
157 {
158 // if we are loading a module and no R path is defined, that's okay
159 // just mark the path as empty and the default R on the module path
160 // will be used instead
161 rScriptPath = FilePath();
162 }
163 }
164
165 // get the prelaunch script to be executed before attempting to load R to read version info
166 // if the prelaunch script is specific to users (starts with ~), don't attempt to use it
167 // as it is likely not available for the RStudio account
168 std::string prelaunchScript = rEntry.prelaunchScript();
169 if (prelaunchScript.find('~') == 0)
170 {
171 prelaunchScript = "";
172 }
173
174 std::string rDiscoveredScriptPath, rVersion, errMsg;
175 core::system::Options env;
176 if (detectREnvironment(rScriptPath,
177 ldPathsScript,
178 ldLibraryPath,
179 &rDiscoveredScriptPath,
180 &rVersion,
181 &env,
182 &errMsg,
183 prelaunchScript,
184 rEntry.module(),
185 modulesBinaryPath))
186 {
187 // merge the found environment with the existing user-overridden environment
188 // we ensure that the user overrides overwrite whatever environment we established automatically
189 core::system::Options userEnv = rEntry.environment();
190 core::system::Options mergedEnv;
191
192 // set automatically found variables first
193 for (const core::system::Option& option : env)
194 {
195 core::system::setenv(&mergedEnv, option.first, option.second);
196 }
197
198 // override them with whatever was explicitly set by the user
199 for (const core::system::Option& option : userEnv)
200 {
201 // do not override R_HOME as it was corrected while detecting the environment
202 // this is necessary because the user-specified path might be just the root directory
203 // and not the full install directory
204 if (option.first == "R_HOME")
205 continue;
206
207 core::system::setenv(&mergedEnv, option.first, option.second);
208 }
209
210 rEntry.setNumber(rVersion);
211 rEntry.setEnvironment(mergedEnv);
212
213 rVersions.push_back(rEntry);
214 }
215 else
216 {
217 std::string rVersion;
218
219 if (!rEntry.module().empty())
220 rVersion += " module " + rEntry.module();
221 if (!rScriptPath.getAbsolutePath().empty())
222 rVersion += " at " + rScriptPath.getAbsolutePath();
223
224 LOG_ERROR_MESSAGE("Error scanning R version" + rVersion + ": " + errMsg);
225 }
226 }
227
228 // probe versions
229 for (const FilePath& rHomePath : rHomePaths)
230 {
231 // compute R script path
232 FilePath rScriptPath = rHomePath.completeChildPath("bin/R");
233 if (!rScriptPath.exists())
234 continue;
235
236 std::string rDiscoveredScriptPath, rVersion, errMsg;
237 core::system::Options env;
238 if (detectREnvironment(rScriptPath,
239 ldPathsScript,
240 ldLibraryPath,
241 &rDiscoveredScriptPath,
242 &rVersion,
243 &env,
244 &errMsg))
245 {
246 RVersion version(rVersion, env);
247 rVersions.push_back(version);
248 }
249 else
250 {
251 LOG_ERROR_MESSAGE("Error scanning R version at " +
252 rScriptPath.getAbsolutePath() + ": " +
253 errMsg);
254 }
255 }
256
257 #ifdef __APPLE__
258 // scan the R frameworks directory
259 FilePath rFrameworkVersions(kRFrameworkVersions);
260 std::vector<FilePath> versionPaths;
261 Error error = rFrameworkVersions.getChildren(versionPaths);
262 if (error)
263 LOG_ERROR(error);
264 for (const FilePath& versionPath : versionPaths)
265 {
266 if (!versionPath.isHidden() && (versionPath.getFilename() != "Current"))
267 {
268 using namespace rstudio::core::system;
269 core::system::Options env;
270 FilePath rHomePath = versionPath.completeChildPath("Resources");
271 FilePath rLibPath = rHomePath.completeChildPath("lib");
272 core::system::setenv(&env, "R_HOME", rHomePath.getAbsolutePath());
273 core::system::setenv(&env,
274 "R_SHARE_DIR",
275 rHomePath.completeChildPath("share").getAbsolutePath());
276 core::system::setenv(&env,
277 "R_INCLUDE_DIR",
278 rHomePath.completeChildPath("include").getAbsolutePath());
279 core::system::setenv(&env,
280 "R_DOC_DIR",
281 rHomePath.completeChildPath("doc").getAbsolutePath());
282 core::system::setenv(&env,
283 "DYLD_FALLBACK_LIBRARY_PATH",
284 r_util::rLibraryPath(rHomePath,
285 rLibPath,
286 ldPathsScript,
287 ldLibraryPath));
288 core::system::setenv(&env, "R_ARCH", "/x86_64");
289
290 RVersion version(versionPath.getFilename(), env);
291
292 // improve on the version by asking R for it's version
293 FilePath rBinaryPath = rHomePath.completeChildPath("bin/exec/R");
294 if (!rBinaryPath.exists())
295 rBinaryPath = rHomePath.completeChildPath("bin/exec/x86_64/R");
296 if (rBinaryPath.exists())
297 {
298 std::string versionNumber = version.number();
299 Error error = rVersion(rHomePath,
300 rBinaryPath,
301 ldLibraryPath,
302 &versionNumber);
303 if (error)
304 LOG_ERROR(error);
305 version = RVersion(versionNumber, version.environment());
306 }
307
308 rVersions.push_back(version);
309 }
310 }
311 #endif
312
313 // sort the versions using stable sort
314 // this gaurantees that versions specified in the versions file will come first
315 // this makes sure that versions that have user-defined metadata (such as labels)
316 // will not be erased in the subsequent erase call, but the equivalent default versions that were
317 // found will be erased instead
318 std::stable_sort(rVersions.begin(), rVersions.end());
319
320 // remove duplicates
321 rVersions.erase(std::unique(rVersions.begin(), rVersions.end()),
322 rVersions.end());
323
324 // reverse the order so more recent versions come first
325 std::reverse(rVersions.begin(), rVersions.end());
326
327 // return the versions
328 return rVersions;
329 }
330
331 namespace {
332
isVersion(const RVersionNumber & number,const std::string & rHomeDir,const RVersion & item)333 bool isVersion(const RVersionNumber& number,
334 const std::string& rHomeDir,
335 const RVersion& item)
336 {
337 return number == RVersionNumber::parse(item.number()) &&
338 rHomeDir == item.homeDir().getAbsolutePath();
339 }
340
isLabelVersion(const RVersionNumber & number,const std::string & rHomeDir,const std::string & label,const RVersion & item)341 bool isLabelVersion(const RVersionNumber& number,
342 const std::string& rHomeDir,
343 const std::string& label,
344 const RVersion& item)
345 {
346 return isVersion(number, rHomeDir, item) &&
347 label == item.label();
348 }
349
isMajorMinorVersion(const RVersionNumber & test,const RVersion & item)350 bool isMajorMinorVersion(const RVersionNumber& test, const RVersion& item)
351 {
352 RVersionNumber itemNumber = RVersionNumber::parse(item.number());
353 return (test.versionMajor() == itemNumber.versionMajor() &&
354 test.versionMinor() == itemNumber.versionMinor());
355 }
356
357
compareVersionInfo(const RVersionNumber & versionNumber,const RVersion & version)358 bool compareVersionInfo(const RVersionNumber& versionNumber,
359 const RVersion& version)
360 {
361 return versionNumber < RVersionNumber::parse(version.number());
362 }
363
findClosest(const RVersionNumber & versionNumber,std::vector<RVersion> versions)364 RVersion findClosest(const RVersionNumber& versionNumber,
365 std::vector<RVersion> versions)
366 {
367 // sort so algorithms work correctly
368 std::sort(versions.begin(), versions.end());
369
370 // first look for an upper_bound
371 std::vector<RVersion>::const_iterator it;
372 it = std::upper_bound(versions.begin(),
373 versions.end(),
374 versionNumber,
375 compareVersionInfo);
376 if (it != versions.end())
377 return *it;
378
379 // can't find a greater version, use the newest version
380 return *std::max_element(versions.begin(), versions.end());
381 }
382
383 }
384
385
selectVersion(const std::string & number,const std::string & rHomeDir,const std::string & label,std::vector<RVersion> versions)386 RVersion selectVersion(const std::string& number,
387 const std::string& rHomeDir,
388 const std::string& label,
389 std::vector<RVersion> versions)
390 {
391 // check for empty
392 if (versions.empty())
393 return RVersion();
394
395 // version we are seeking
396 RVersionNumber matchNumber = RVersionNumber::parse(number);
397
398 // order correctly for algorithms
399 std::sort(versions.begin(), versions.end());
400
401 // first seek an exact match
402 std::vector<RVersion>::const_iterator it;
403 it = std::find_if(versions.begin(),
404 versions.end(),
405 boost::bind(isLabelVersion, matchNumber, rHomeDir, label, _1));
406 if (it != versions.end())
407 return *it;
408
409 // no exact match (including label)
410 // relax the search to find a matching version with a different label
411 it = std::find_if(versions.begin(),
412 versions.end(),
413 boost::bind(isVersion, matchNumber, rHomeDir, _1));
414 if (it != versions.end())
415 return *it;
416
417 // now look for versions that match major and minor (same series)
418 std::vector<RVersion> seriesVersions;
419 algorithm::copy_if(versions.begin(),
420 versions.end(),
421 std::back_inserter(seriesVersions),
422 boost::bind(isMajorMinorVersion, matchNumber, _1));
423
424 // find the closest match in the series
425 if (seriesVersions.size() > 0)
426 {
427 return findClosest(matchNumber, seriesVersions);
428 }
429 // otherwise find the closest match in the whole list
430 else
431 {
432 return findClosest(matchNumber, versions);
433 }
434 }
435
rVersionToJson(const RVersion & version)436 json::Object rVersionToJson(const RVersion& version)
437 {
438 json::Object versionJson;
439 versionJson["number"] = version.number();
440 versionJson["environment"] = json::Object(version.environment());
441 versionJson["label"] = version.label();
442 versionJson["module"] = version.module();
443 versionJson["prelaunchScript"] = version.prelaunchScript();
444 versionJson["repo"] = version.repo();
445 versionJson["library"] = version.library();
446
447 return versionJson;
448 }
449
rVersionFromJson(const json::Object & versionJson,r_util::RVersion * pVersion)450 Error rVersionFromJson(const json::Object& versionJson,
451 r_util::RVersion* pVersion)
452 {
453 std::string number;
454 json::Object environmentJson;
455 std::string label;
456 std::string module;
457 std::string prelaunchScript;
458 std::string repo;
459 std::string library;
460
461 Error error = json::readObject(versionJson,
462 "number", number,
463 "environment", environmentJson,
464 "label", label,
465 "module", module,
466 "prelaunchScript", prelaunchScript,
467 "repo", repo,
468 "library", library);
469 if (error)
470 return error;
471
472 *pVersion = RVersion(number, environmentJson.toStringPairList());
473
474 pVersion->setLabel(label);
475 pVersion->setModule(module);
476 pVersion->setPrelaunchScript(prelaunchScript);
477 pVersion->setRepo(repo);
478 pVersion->setLibrary(library);
479
480 return Success();
481 }
482
versionsToJson(const std::vector<RVersion> & versions)483 json::Array versionsToJson(const std::vector<RVersion>& versions)
484 {
485 json::Array versionsJson;
486 std::transform(versions.begin(),
487 versions.end(),
488 std::back_inserter(versionsJson),
489 rVersionToJson);
490 return versionsJson;
491 }
492
rVersionsFromJson(const json::Array & versionsJson,std::vector<RVersion> * pVersions)493 Error rVersionsFromJson(const json::Array& versionsJson,
494 std::vector<RVersion>* pVersions)
495 {
496 for (const json::Value& versionJson : versionsJson)
497 {
498 if (!json::isType<json::Object>(versionJson))
499 return systemError(boost::system::errc::bad_message, ERROR_LOCATION);
500
501 r_util::RVersion rVersion;
502 Error error = rVersionFromJson(versionJson.getObject(), &rVersion);
503 if (error)
504 return error;
505
506 pVersions->push_back(rVersion);
507 }
508
509 return Success();
510 }
511
512
writeRVersionsToFile(const FilePath & filePath,const std::vector<r_util::RVersion> & versions)513 Error writeRVersionsToFile(const FilePath& filePath,
514 const std::vector<r_util::RVersion>& versions)
515 {
516 return core::writeStringToFile(filePath, versionsToJson(versions).writeFormatted());
517 }
518
readRVersionsFromFile(const FilePath & filePath,std::vector<r_util::RVersion> * pVersions)519 Error readRVersionsFromFile(const FilePath& filePath,
520 std::vector<r_util::RVersion>* pVersions)
521 {
522 // read file contents
523 std::string contents;
524 Error error = core::readStringFromFile(filePath, &contents);
525 if (error)
526 return error;
527
528 // parse json
529 using namespace json;
530 json::Value jsonValue;
531 if (jsonValue.parse(contents) || !isType<json::Array>(jsonValue))
532 {
533 Error error = systemError(boost::system::errc::bad_message,
534 ERROR_LOCATION);
535 error.addProperty("contents", contents);
536 return error;
537 }
538
539 return rVersionsFromJson(jsonValue.getArray(), pVersions);
540 }
541
validatedReadRVersionsFromFile(const FilePath & filePath,std::vector<r_util::RVersion> * pVersions)542 Error validatedReadRVersionsFromFile(const FilePath& filePath,
543 std::vector<r_util::RVersion>* pVersions)
544 {
545 std::vector<r_util::RVersion> versions;
546 Error error = readRVersionsFromFile(filePath, &versions);
547 if (error)
548 return error;
549
550 // ensure the home path exists before returning
551 for (const r_util::RVersion& version : versions)
552 {
553 if (version.homeDir().exists())
554 {
555 pVersions->push_back(version);
556 }
557 else
558 {
559 LOG_WARNING_MESSAGE("R version home directory not found: " +
560 version.homeDir().getAbsolutePath());
561 }
562 }
563
564 return Success();
565 }
566
567
568 } // namespace r_util
569 } // namespace core
570 } // namespace rstudio
571
572
573
574