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