1 /*
2  * SessionBuild.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 "SessionBuild.hpp"
17 
18 #include "session-config.h"
19 
20 #include <vector>
21 
22 #include <boost/utility.hpp>
23 #include <boost/shared_ptr.hpp>
24 #include <boost/format.hpp>
25 #include <boost/scope_exit.hpp>
26 #include <boost/enable_shared_from_this.hpp>
27 #include <boost/algorithm/string/split.hpp>
28 #include <boost/algorithm/string/join.hpp>
29 
30 #include <core/Exec.hpp>
31 #include <core/FileSerializer.hpp>
32 #include <core/Version.hpp>
33 #include <core/text/DcfParser.hpp>
34 #include <core/system/Process.hpp>
35 #include <core/system/Environment.hpp>
36 #include <core/system/ShellUtils.hpp>
37 #include <core/r_util/RPackageInfo.hpp>
38 
39 #include <session/SessionOptions.hpp>
40 
41 #ifdef _WIN32
42 #include <core/r_util/RToolsInfo.hpp>
43 #endif
44 
45 #include <r/RExec.hpp>
46 #include <r/ROptions.hpp>
47 #include <r/RRoutines.hpp>
48 #include <r/RUtil.hpp>
49 #include <r/session/RSessionUtils.hpp>
50 #include <r/session/RConsoleHistory.hpp>
51 
52 #include <session/projects/SessionProjects.hpp>
53 #include <session/SessionModuleContext.hpp>
54 #include <session/SessionQuarto.hpp>
55 #include <session/prefs/UserPrefs.hpp>
56 
57 #include "SessionBuildErrors.hpp"
58 #include "SessionSourceCpp.hpp"
59 #include "SessionInstallRtools.hpp"
60 
61 using namespace rstudio::core;
62 
63 namespace rstudio {
64 namespace session {
65 
66 namespace {
67 
68 static bool s_canBuildCpp = false;
69 
preflightPackageBuildErrorMessage(const std::string & message,const FilePath & buildDirectory)70 std::string preflightPackageBuildErrorMessage(
71       const std::string& message,
72       const FilePath& buildDirectory)
73 {
74    std::string fmt =
75 R"EOF(ERROR: Package build failed.
76 
77 %1%
78 
79 Build directory: %2%
80 )EOF";
81 
82    auto formatter = boost::format(fmt)
83          % message
84          % module_context::createAliasedPath(buildDirectory);
85    return boost::str(formatter);
86 }
87 
quoteString(const std::string & str)88 std::string quoteString(const std::string& str)
89 {
90    return "'" + str + "'";
91 }
92 
packageArgsVector(std::string args)93 std::string packageArgsVector(std::string args)
94 {
95    // spilt the string
96    boost::algorithm::trim(args);
97    std::vector<std::string> argList;
98    boost::algorithm::split(argList,
99                            args,
100                            boost::is_space(),
101                            boost::algorithm::token_compress_on);
102 
103    // quote the args
104    std::vector<std::string> quotedArgs;
105    std::transform(argList.begin(),
106                   argList.end(),
107                   std::back_inserter(quotedArgs),
108                   quoteString);
109 
110    std::ostringstream ostr;
111    ostr << "c(" << boost::algorithm::join(quotedArgs, ",") << ")";
112    return ostr.str();
113 }
114 
isPackageBuildError(const std::string & output)115 bool isPackageBuildError(const std::string& output)
116 {
117    std::string input = boost::algorithm::trim_copy(output);
118    return boost::algorithm::istarts_with(input, "warning: ") ||
119           boost::algorithm::istarts_with(input, "error: ") ||
120           boost::algorithm::ends_with(input, "WARNING");
121 }
122 
123 
124 } // anonymous namespace
125 
126 namespace modules {
127 namespace build {
128 
129 namespace {
130 
131 // track whether to force a package rebuild. we do this if the user
132 // saves a header file (since the R CMD INSTALL makefile doesn't
133 // force a rebuild for those changes)
134 bool s_forcePackageRebuild = false;
135 
isPackageHeaderFile(const FilePath & filePath)136 bool isPackageHeaderFile(const FilePath& filePath)
137 {
138    if (projects::projectContext().hasProject() &&
139        (projects::projectContext().config().buildType ==
140                                               r_util::kBuildTypePackage) &&
141        (boost::algorithm::starts_with(filePath.getExtensionLowerCase(), ".h") ||
142         filePath.getExtensionLowerCase() == ".stan"))
143    {
144       FilePath pkgPath = projects::projectContext().buildTargetPath();
145       std::string pkgRelative = filePath.getRelativePath(pkgPath);
146       if (boost::algorithm::starts_with(pkgRelative, "src"))
147          return true;
148       else if (boost::algorithm::starts_with(pkgRelative, "inst/include"))
149         return true;
150    }
151 
152    return false;
153 }
154 
onFileChanged(FilePath sourceFilePath)155 void onFileChanged(FilePath sourceFilePath)
156 {
157    // set package rebuild flag
158    if (!s_forcePackageRebuild)
159    {
160       if (isPackageHeaderFile(sourceFilePath))
161          s_forcePackageRebuild = true;
162    }
163 }
164 
onSourceEditorFileSaved(FilePath sourceFilePath)165 void onSourceEditorFileSaved(FilePath sourceFilePath)
166 {
167    onFileChanged(sourceFilePath);
168 
169    // see if this is a website file and fire an event if it is
170    if (module_context::isWebsiteProject())
171    {
172       // see if the option is enabled for live preview
173       projects::RProjectBuildOptions options;
174       Error error = projects::projectContext().readBuildOptions(&options);
175       if (error)
176       {
177          LOG_ERROR(error);
178          return;
179       }
180 
181       FilePath buildTargetPath = projects::projectContext().buildTargetPath();
182       if (sourceFilePath.isWithin(buildTargetPath))
183       {
184          std::string outputDir = module_context::websiteOutputDir();
185          FilePath outputDirPath = buildTargetPath.completeChildPath(outputDir);
186          if (outputDir.empty() || !sourceFilePath.isWithin(outputDirPath))
187          {
188             // are we live previewing?
189             bool livePreview = options.livePreviewWebsite;
190 
191             // force live preview for JS and CSS
192             std::string mimeType = sourceFilePath.getMimeContentType();
193             if (mimeType == "text/css" || mimeType == "text/javascript")
194                livePreview = true;
195 
196             if (livePreview)
197             {
198                json::Object fileJson =
199                    module_context::createFileSystemItem(sourceFilePath);
200                ClientEvent event(client_events::kWebsiteFileSaved, fileJson);
201                module_context::enqueClientEvent(event);
202             }
203          }
204       }
205    }
206 }
207 
onFilesChanged(const std::vector<core::system::FileChangeEvent> & events)208 void onFilesChanged(const std::vector<core::system::FileChangeEvent>& events)
209 {
210    if (!s_forcePackageRebuild)
211    {
212       for (const auto &event : events) {
213          FilePath filePath(event.fileInfo().absolutePath());
214          onFileChanged(filePath);
215       }
216    }
217 }
218 
collectForcePackageRebuild()219 bool collectForcePackageRebuild()
220 {
221    if (s_forcePackageRebuild)
222    {
223       s_forcePackageRebuild = false;
224       return true;
225    }
226    else
227    {
228       return false;
229    }
230 }
231 
232 
233 const char * const kRoxygenizePackage = "roxygenize-package";
234 const char * const kBuildSourcePackage = "build-source-package";
235 const char * const kBuildBinaryPackage = "build-binary-package";
236 const char * const kTestPackage = "test-package";
237 const char * const kCheckPackage = "check-package";
238 const char * const kBuildAndReload = "build-all";
239 const char * const kRebuildAll = "rebuild-all";
240 const char * const kTestFile = "test-file";
241 const char * const kTestShiny = "test-shiny";
242 const char * const kTestShinyFile = "test-shiny-file";
243 
244 class Build : boost::noncopyable,
245               public boost::enable_shared_from_this<Build>
246 {
247 public:
create(const std::string & type,const std::string & subType)248    static boost::shared_ptr<Build> create(const std::string& type,
249                                           const std::string& subType)
250    {
251       boost::shared_ptr<Build> pBuild(new Build());
252       pBuild->start(type, subType);
253       return pBuild;
254    }
255 
256 private:
Build()257    Build()
258       : isRunning_(false), terminationRequested_(false), restartR_(false),
259         usedDevtools_(false), openErrorList_(true)
260    {
261    }
262 
start(const std::string & type,const std::string & subType)263    void start(const std::string& type, const std::string& subType)
264    {
265       json::Object dataJson;
266       dataJson["type"] = type;
267       dataJson["sub_type"] = subType;
268       ClientEvent event(client_events::kBuildStarted, dataJson);
269       module_context::enqueClientEvent(event);
270 
271       isRunning_ = true;
272 
273       // read build options
274       Error error = projects::projectContext().readBuildOptions(&options_);
275       if (error)
276       {
277          terminateWithError("reading build options file", error);
278          return;
279       }
280 
281       // callbacks
282       core::system::ProcessCallbacks cb;
283       cb.onContinue = boost::bind(&Build::onContinue,
284                                   Build::shared_from_this());
285       cb.onStdout = boost::bind(&Build::onStandardOutput,
286                                 Build::shared_from_this(), _2);
287       cb.onStderr = boost::bind(&Build::onStandardError,
288                                 Build::shared_from_this(), _2);
289       cb.onExit =  boost::bind(&Build::onCompleted,
290                                 Build::shared_from_this(),
291                                 _1);
292 
293       // execute build
294       executeBuild(type, subType, cb);
295    }
296 
297 
executeBuild(const std::string & type,const std::string & subType,const core::system::ProcessCallbacks & cb)298    void executeBuild(const std::string& type,
299                      const std::string& subType,
300                      const core::system::ProcessCallbacks& cb)
301    {
302       // options
303       core::system::ProcessOptions options;
304 
305 #ifndef _WIN32
306       options.terminateChildren = true;
307 #endif
308 
309       // notify build process of build-pane width
310       core::system::Options environment;
311       core::system::environment(&environment);
312       int buildWidth = r::options::getBuildOptionWidth();
313       if (buildWidth > 0)
314          core::system::setenv(&environment, "RSTUDIO_CONSOLE_WIDTH",
315                               safe_convert::numberToString(buildWidth));
316       else
317          core::system::unsetenv(&environment, "RSTUDIO_CONSOLE_WIDTH");
318 
319       FilePath buildTargetPath = projects::projectContext().buildTargetPath();
320       const core::r_util::RProjectConfig& config = projectConfig();
321       if (type == kTestFile)
322       {
323          options.environment = environment;
324          options.workingDir = buildTargetPath.getParent();
325          FilePath testPath = FilePath(subType);
326          executePackageBuild(type, testPath, options, cb);
327       }
328       else if (type == kTestShiny || type == kTestShinyFile)
329       {
330          FilePath testPath = FilePath(subType);
331          testShiny(testPath, options, cb, type);
332       }
333       else if (config.buildType == r_util::kBuildTypePackage)
334       {
335          options.environment = environment;
336          options.workingDir = buildTargetPath.getParent();
337          executePackageBuild(type, buildTargetPath, options, cb);
338       }
339       else if (config.buildType == r_util::kBuildTypeMakefile)
340       {
341          options.environment = environment;
342          options.workingDir = buildTargetPath;
343          executeMakefileBuild(type, buildTargetPath, options, cb);
344       }
345       else if (config.buildType == r_util::kBuildTypeWebsite)
346       {
347          options.workingDir = buildTargetPath;
348 
349          // pass along R_LIBS
350          std::string rLibs = module_context::libPathsString();
351          if (!rLibs.empty())
352             core::system::setenv(&environment, "R_LIBS", rLibs);
353 
354          // pass along RSTUDIO_VERSION
355          core::system::setenv(&environment, "RSTUDIO_VERSION", RSTUDIO_VERSION);
356 
357          options.environment = environment;
358 
359          executeWebsiteBuild(type, subType, buildTargetPath, options, cb);
360       }
361       else if (config.buildType == r_util::kBuildTypeCustom)
362       {
363          options.environment = environment;
364          options.workingDir = buildTargetPath.getParent();
365          executeCustomBuild(type, buildTargetPath, options, cb);
366       }
367       else if (quarto::quartoConfig().is_project)
368       {
369          options.environment = environment;
370          options.workingDir = projects::projectContext().directory();
371          executeQuartoBuild(subType, options, cb);
372       }
373       else
374       {
375          terminateWithError("Unrecognized build type: " + config.buildType);
376       }
377    }
378 
executePackageBuild(const std::string & type,const FilePath & packagePath,const core::system::ProcessOptions & options,const core::system::ProcessCallbacks & cb)379    void executePackageBuild(const std::string& type,
380                             const FilePath& packagePath,
381                             const core::system::ProcessOptions& options,
382                             const core::system::ProcessCallbacks& cb)
383    {
384       if (type == kTestFile)
385       {
386           // try to read package from /tests/testthat/filename.R,
387           // but ignore errors if not within a package
388           FilePath maybePackage = module_context::resolveAliasedPath(
389              packagePath.getParent().getParent().getParent().getAbsolutePath()
390           );
391 
392           pkgInfo_.read(maybePackage);
393       }
394       else
395       {
396          // validate that this is a package
397          if (!packagePath.completeChildPath("DESCRIPTION").exists())
398          {
399             std::string message =
400                   "The build directory does not contain a DESCRIPTION file and so "
401                   "cannot be built as a package.";
402 
403             terminateWithError(preflightPackageBuildErrorMessage(message, packagePath));
404             return;
405          }
406 
407          // get package info
408          Error error = pkgInfo_.read(packagePath);
409          if (error)
410          {
411             // check to see if this was a parse error; if so, report that
412             std::string parseError = error.getProperty("parse-error");
413             if (!parseError.empty())
414             {
415                std::string message = "Failed to parse DESCRIPTION: " + parseError;
416                terminateWithError(preflightPackageBuildErrorMessage(message, packagePath));
417             }
418             else
419             {
420                terminateWithError("reading package DESCRIPTION", error);
421             }
422 
423             return;
424          }
425 
426          // if this package links to Rcpp then we run compileAttributes
427          if (pkgInfo_.linkingTo().find("Rcpp") != std::string::npos)
428             if (!compileRcppAttributes(packagePath))
429                return;
430       }
431 
432       if (type == kRoxygenizePackage)
433       {
434          successMessage_ = "Documentation completed";
435          roxygenize(packagePath, options, cb);
436       }
437       else
438       {
439          // bind a function that can be used to build the package
440          boost::function<void()> buildFunction = boost::bind(
441                          &Build::buildPackage, Build::shared_from_this(),
442                          type, packagePath, options, cb);
443 
444          if (roxygenizeRequired(type))
445          {
446             // special callback for roxygenize result
447             core::system::ProcessCallbacks roxygenizeCb = cb;
448             roxygenizeCb.onExit =  boost::bind(&Build::onRoxygenizeCompleted,
449                                                Build::shared_from_this(),
450                                                _1,
451                                                buildFunction);
452 
453             // run it
454             roxygenize(packagePath, options, roxygenizeCb);
455          }
456          else
457          {
458             buildFunction();
459          }
460       }
461    }
462 
roxygenizeRequired(const std::string & type)463    bool roxygenizeRequired(const std::string& type)
464    {
465       if (!projectConfig().packageRoxygenize.empty())
466       {
467          if ((type == kBuildAndReload || type == kRebuildAll) &&
468              options_.autoRoxygenizeForBuildAndReload)
469          {
470             return true;
471          }
472          else if ( (type == kBuildSourcePackage ||
473                     type == kBuildBinaryPackage) &&
474                    options_.autoRoxygenizeForBuildPackage)
475          {
476             return true;
477          }
478          else if ( (type == kCheckPackage) &&
479                    options_.autoRoxygenizeForCheck &&
480                    !useDevtools())
481          {
482             return true;
483          }
484          else
485          {
486             return false;
487          }
488       }
489       else
490       {
491          return false;
492       }
493    }
494 
495 
buildRoxygenizeCall()496    std::string buildRoxygenizeCall()
497    {
498       // build the call to roxygenize
499       std::vector<std::string> roclets;
500       boost::algorithm::split(roclets,
501                               projectConfig().packageRoxygenize,
502                               boost::algorithm::is_any_of(","));
503 
504       // remove vignette roclet if we don't have the requisite roxygen2 version
505       bool haveVignetteRoclet = module_context::isPackageVersionInstalled(
506                                                    "roxygen2", "4.1.0.9001");
507       if (!haveVignetteRoclet)
508       {
509          auto it = std::find(roclets.begin(), roclets.end(), "vignette");
510          if (it != roclets.end())
511             roclets.erase(it);
512       }
513 
514       for (std::string& roclet : roclets)
515       {
516          roclet = "'" + roclet + "'";
517       }
518 
519       boost::format fmt;
520       if (useDevtools())
521          fmt = boost::format("devtools::document(roclets = c(%1%))");
522       else
523          fmt = boost::format("roxygen2::roxygenize('.', roclets = c(%1%))");
524       std::string roxygenizeCall = boost::str(
525          fmt % boost::algorithm::join(roclets, ", "));
526 
527       // show the user the call to roxygenize
528       enqueCommandString(roxygenizeCall);
529 
530       // format the command to send to R
531       boost::format cmdFmt(
532          "suppressPackageStartupMessages("
533             "{oldLC <- Sys.getlocale(category = 'LC_COLLATE'); "
534             " Sys.setlocale(category = 'LC_COLLATE', locale = 'C'); "
535             " on.exit(Sys.setlocale(category = 'LC_COLLATE', locale = oldLC));"
536             " %1%; }"
537           ")");
538       return boost::str(cmdFmt % roxygenizeCall);
539    }
540 
onRoxygenizeCompleted(int exitStatus,const boost::function<void ()> & buildFunction)541    void onRoxygenizeCompleted(int exitStatus,
542                               const boost::function<void()>& buildFunction)
543    {
544       if (exitStatus == EXIT_SUCCESS)
545       {
546          std::string msg = "Documentation completed\n\n";
547          enqueBuildOutput(module_context::kCompileOutputNormal, msg);
548          buildFunction();
549       }
550       else
551       {
552          terminateWithErrorStatus(exitStatus);
553       }
554    }
555 
556 
roxygenize(const FilePath & packagePath,core::system::ProcessOptions options,const core::system::ProcessCallbacks & cb)557    void roxygenize(const FilePath& packagePath,
558                    core::system::ProcessOptions options,
559                    const core::system::ProcessCallbacks& cb)
560    {
561       FilePath rScriptPath;
562       Error error = module_context::rScriptPath(&rScriptPath);
563       if (error)
564       {
565          terminateWithError("Locating R script", error);
566          return;
567       }
568 
569       // check for required version of roxygen
570       if (!module_context::isMinimumRoxygenInstalled())
571       {
572          terminateWithError("roxygen2 v4.0 (or later) required to "
573                             "generate documentation");
574       }
575 
576       // make a copy of options so we can customize the environment
577       core::system::Options childEnv;
578       if (options.environment)
579          childEnv = *options.environment;
580       else
581          core::system::environment(&childEnv);
582 
583       // allow child process to inherit our R_LIBS
584       std::string libPaths = module_context::libPathsString();
585       if (!libPaths.empty())
586          core::system::setenv(&childEnv, "R_LIBS", libPaths);
587 
588       options.environment = childEnv;
589 
590       // build the roxygenize command
591       shell_utils::ShellCommand cmd(rScriptPath);
592       cmd << "--vanilla";
593       cmd << "-s";
594       cmd << "-e";
595       cmd << buildRoxygenizeCall();
596 
597       // use the package working dir
598       options.workingDir = packagePath;
599 
600       // run it
601       module_context::processSupervisor().runCommand(cmd,
602                                                      options,
603                                                      cb);
604    }
605 
compileRcppAttributes(const FilePath & packagePath)606    bool compileRcppAttributes(const FilePath& packagePath)
607    {
608       if (module_context::haveRcppAttributes())
609       {
610          core::system::ProcessResult result;
611          Error error = module_context::sourceModuleRFileWithResult(
612                                              "SessionCompileAttributes.R",
613                                              packagePath,
614                                              &result);
615          if (error)
616          {
617             LOG_ERROR(error);
618             enqueCommandString("Rcpp::compileAttributes()");
619             terminateWithError(r::endUserErrorMessage(error));
620             return false;
621          }
622          else if (!result.stdOut.empty() || !result.stdErr.empty())
623          {
624             enqueCommandString("Rcpp::compileAttributes()");
625             enqueBuildOutput(module_context::kCompileOutputNormal,
626                              result.stdOut);
627             if (!result.stdErr.empty())
628                enqueBuildOutput(module_context::kCompileOutputError,
629                                 result.stdErr);
630             enqueBuildOutput(module_context::kCompileOutputNormal, "\n");
631             if (result.exitStatus == EXIT_SUCCESS)
632             {
633                return true;
634             }
635             else
636             {
637                terminateWithErrorStatus(result.exitStatus);
638                return false;
639             }
640          }
641          else
642          {
643             return true;
644          }
645       }
646       else
647       {
648          return true;
649       }
650    }
651 
buildPackage(const std::string & type,const FilePath & packagePath,const core::system::ProcessOptions & options,const core::system::ProcessCallbacks & cb)652    void buildPackage(const std::string& type,
653                      const FilePath& packagePath,
654                      const core::system::ProcessOptions& options,
655                      const core::system::ProcessCallbacks& cb)
656    {
657 
658       // if this action is going to INSTALL the package then on
659       // windows we need to unload the library first
660 #ifdef _WIN32
661       if (packagePath.completeChildPath("src").exists() &&
662          (type == kBuildAndReload || type == kRebuildAll ||
663           type == kBuildBinaryPackage))
664       {
665          std::string pkg = pkgInfo_.name();
666          Error error = r::exec::RFunction(".rs.forceUnloadPackage", pkg).call();
667          if (error)
668             LOG_ERROR(error);
669       }
670 #endif
671 
672       // use both the R and gcc error parsers
673       CompileErrorParsers parsers;
674       parsers.add(rErrorParser(packagePath.completePath("R")));
675       parsers.add(gccErrorParser(packagePath.completePath("src")));
676 
677       // track build type
678       type_ = type;
679 
680       // add testthat and shinytest result parsers
681       core::Version testthatVersion;
682       module_context::packageVersion("testthat", &testthatVersion);
683 
684       if (type == kTestFile)
685       {
686          openErrorList_ = false;
687          parsers.add(testthatErrorParser(packagePath.getParent(), testthatVersion));
688       }
689       else if (type == kTestPackage)
690       {
691          openErrorList_ = false;
692          parsers.add(testthatErrorParser(packagePath.completePath("tests/testthat"), testthatVersion));
693       }
694 
695       initErrorParser(packagePath, parsers);
696 
697       // make a copy of options so we can customize the environment
698       core::system::ProcessOptions pkgOptions(options);
699       core::system::Options childEnv;
700       if (options.environment)
701          childEnv = *options.environment;
702       else
703          core::system::environment(&childEnv);
704 
705       // allow child process to inherit our R_LIBS
706       std::string libPaths = module_context::libPathsString();
707       if (!libPaths.empty())
708          core::system::setenv(&childEnv, "R_LIBS", libPaths);
709 
710       // record the library paths used when this build was kicked off
711       libPaths_ = module_context::getLibPaths();
712 
713       // prevent spurious cygwin warnings on windows
714 #ifdef _WIN32
715       core::system::setenv(&childEnv, "CYGWIN", "nodosfilewarning");
716 #endif
717 
718       // set the not cran env var
719       core::system::setenv(&childEnv, "NOT_CRAN", "true");
720 
721       // turn off external applications launching
722       core::system::setenv(&childEnv, "R_BROWSER", "false");
723       core::system::setenv(&childEnv, "R_PDFVIEWER", "false");
724 
725       // add r tools to path if necessary
726       module_context::addRtoolsToPathIfNecessary(&childEnv, &buildToolsWarning_);
727 
728       pkgOptions.environment = childEnv;
729 
730       // get R bin directory
731       FilePath rBinDir;
732       Error error = module_context::rBinDir(&rBinDir);
733       if (error)
734       {
735          terminateWithError("attempting to locate R binary", error);
736          return;
737       }
738 
739       // install an error filter (because R package builds produce much
740       // of their output on stderr)
741       errorOutputFilterFunction_ = isPackageBuildError;
742 
743       // build command
744       if (type == kBuildAndReload || type == kRebuildAll)
745       {
746          // restart R after build is completed
747          restartR_ = true;
748 
749          // build command
750          module_context::RCommand rCmd(rBinDir);
751          rCmd << "INSTALL";
752 
753          // get extra args
754          std::string extraArgs = projectConfig().packageInstallArgs;
755 
756          // add --preclean if this is a rebuild all
757          if (collectForcePackageRebuild() || (type == kRebuildAll))
758          {
759             if (!boost::algorithm::contains(extraArgs, "--preclean"))
760                rCmd << "--preclean";
761          }
762 
763          // remove --with-keep.source if this is R < 2.14
764          if (!r::util::hasRequiredVersion("2.14"))
765          {
766             using namespace boost::algorithm;
767             replace_all(extraArgs, "--with-keep.source", "");
768             replace_all(extraArgs, "--without-keep.source", "");
769          }
770 
771          // add extra args if provided
772          rCmd << extraArgs;
773 
774          // add filename as a FilePath so it is escaped
775          rCmd << FilePath(packagePath.getFilename());
776 
777          // show the user the command
778          enqueCommandString(rCmd.commandString());
779 
780          // run R CMD INSTALL <package-dir>
781          module_context::processSupervisor().runCommand(rCmd.shellCommand(),
782                                                         pkgOptions,
783                                                         cb);
784       }
785 
786       else if (type == kBuildSourcePackage)
787       {
788          if (useDevtools())
789          {
790             devtoolsBuildPackage(packagePath, false, pkgOptions, cb);
791          }
792          else
793          {
794             if (session::options().packageOutputInPackageFolder())
795             {
796                pkgOptions.workingDir = packagePath;
797             }
798             buildSourcePackage(rBinDir, packagePath, pkgOptions, cb);
799          }
800       }
801 
802       else if (type == kBuildBinaryPackage)
803       {
804          if (useDevtools())
805          {
806             devtoolsBuildPackage(packagePath, true, pkgOptions, cb);
807          }
808          else
809          {
810             if (session::options().packageOutputInPackageFolder())
811             {
812                pkgOptions.workingDir = packagePath;
813             }
814             buildBinaryPackage(rBinDir, packagePath, pkgOptions, cb);
815          }
816       }
817 
818       else if (type == kCheckPackage)
819       {
820          if (useDevtools())
821          {
822             // redirect stderr to stdout for certain build types
823             // see: https://github.com/rstudio/rstudio/issues/5126
824             pkgOptions.redirectStdErrToStdOut = true;
825 
826             devtoolsCheckPackage(packagePath, pkgOptions, cb);
827          }
828          else
829          {
830             if (session::options().packageOutputInPackageFolder())
831             {
832                pkgOptions.workingDir = packagePath;
833             }
834             checkPackage(rBinDir, packagePath, pkgOptions, cb);
835          }
836       }
837 
838       else if (type == kTestPackage)
839       {
840          if (useDevtools())
841          {
842             // redirect stderr to stdout for certain build types
843             // see: https://github.com/rstudio/rstudio/issues/5126
844             pkgOptions.redirectStdErrToStdOut = true;
845 
846             devtoolsTestPackage(packagePath, pkgOptions, cb);
847          }
848          else
849          {
850             testPackage(packagePath, pkgOptions, cb);
851          }
852       }
853 
854       else if (type == kTestFile)
855       {
856          testFile(packagePath, pkgOptions, cb);
857       }
858    }
859 
buildSourcePackage(const FilePath & rBinDir,const FilePath & packagePath,const core::system::ProcessOptions & pkgOptions,const core::system::ProcessCallbacks & cb)860    void buildSourcePackage(const FilePath& rBinDir,
861                            const FilePath& packagePath,
862                            const core::system::ProcessOptions& pkgOptions,
863                            const core::system::ProcessCallbacks& cb)
864    {
865       // compose the build command
866       module_context::RCommand rCmd(rBinDir);
867       rCmd << "build";
868 
869       // add extra args if provided
870       std::string extraArgs = projectConfig().packageBuildArgs;
871       rCmd << extraArgs;
872 
873       // add filename as a FilePath so it is escaped
874       if (session::options().packageOutputInPackageFolder())
875          rCmd << FilePath(".");
876       else
877          rCmd << FilePath(packagePath.getFilename());
878 
879       // show the user the command
880       enqueCommandString(rCmd.commandString());
881 
882       // set a success message
883       successMessage_ = buildPackageSuccessMsg("Source");
884 
885       // run R CMD build <package-dir>
886       module_context::processSupervisor().runCommand(rCmd.shellCommand(),
887                                                      pkgOptions,
888                                                      cb);
889 
890    }
891 
892 
buildBinaryPackage(const FilePath & rBinDir,const FilePath & packagePath,const core::system::ProcessOptions & pkgOptions,const core::system::ProcessCallbacks & cb)893    void buildBinaryPackage(const FilePath& rBinDir,
894                            const FilePath& packagePath,
895                            const core::system::ProcessOptions& pkgOptions,
896                            const core::system::ProcessCallbacks& cb)
897    {
898       // compose the INSTALL --binary
899       module_context::RCommand rCmd(rBinDir);
900       rCmd << "INSTALL";
901       rCmd << "--build";
902       rCmd << "--preclean";
903 
904       // add extra args if provided
905       std::string extraArgs = projectConfig().packageBuildBinaryArgs;
906       rCmd << extraArgs;
907 
908       // add filename as a FilePath so it is escaped
909       if (session::options().packageOutputInPackageFolder())
910          rCmd << FilePath(".");
911       else
912          rCmd << FilePath(packagePath.getFilename());
913 
914       // show the user the command
915       enqueCommandString(rCmd.commandString());
916 
917       // set a success message
918       successMessage_ = "\n" + buildPackageSuccessMsg("Binary");
919 
920       // run R CMD INSTALL --build <package-dir>
921       module_context::processSupervisor().runCommand(rCmd.shellCommand(),
922                                                      pkgOptions,
923                                                      cb);
924    }
925 
checkPackage(const FilePath & rBinDir,const FilePath & packagePath,const core::system::ProcessOptions & pkgOptions,const core::system::ProcessCallbacks & cb)926    void checkPackage(const FilePath& rBinDir,
927                      const FilePath& packagePath,
928                      const core::system::ProcessOptions& pkgOptions,
929                      const core::system::ProcessCallbacks& cb)
930    {
931       // first build then check
932 
933       // compose the build command
934       module_context::RCommand rCmd(rBinDir);
935       rCmd << "build";
936 
937       // add extra args if provided
938       rCmd << projectConfig().packageBuildArgs;
939 
940       // add --no-manual and --no-build-vignettes if they are in the check options
941       std::string checkArgs = projectConfig().packageCheckArgs;
942       if (checkArgs.find("--no-manual") != std::string::npos)
943          rCmd << "--no-manual";
944       if (checkArgs.find("--no-build-vignettes") != std::string::npos)
945          rCmd << "--no-build-vignettes";
946 
947       // add filename as a FilePath so it is escaped
948       if (session::options().packageOutputInPackageFolder())
949          rCmd << FilePath(".");
950       else
951          rCmd << FilePath(packagePath.getFilename());
952 
953       // compose the check command (will be executed by the onExit
954       // handler of the build cmd)
955       module_context::RCommand rCheckCmd(rBinDir);
956       rCheckCmd << "check";
957 
958       // add extra args if provided
959       std::string extraArgs = projectConfig().packageCheckArgs;
960       rCheckCmd << extraArgs;
961 
962       // add filename as a FilePath so it is escaped
963       rCheckCmd << FilePath(pkgInfo_.sourcePackageFilename());
964 
965       // special callback for build result
966       core::system::ProcessCallbacks buildCb = cb;
967       buildCb.onExit =  boost::bind(&Build::onBuildForCheckCompleted,
968                                     Build::shared_from_this(),
969                                     _1,
970                                     rCheckCmd,
971                                     pkgOptions,
972                                     buildCb);
973 
974       // show the user the command
975       enqueCommandString(rCmd.commandString());
976 
977       // set a success message
978       successMessage_ = "R CMD check succeeded\n";
979 
980       // bind a success function if appropriate
981       if (prefs::userPrefs().cleanupAfterRCmdCheck())
982       {
983          successFunction_ = boost::bind(&Build::cleanupAfterCheck,
984                                         Build::shared_from_this(),
985                                         pkgInfo_);
986       }
987 
988       if (prefs::userPrefs().viewDirAfterRCmdCheck())
989       {
990          failureFunction_ = boost::bind(
991                   &Build::viewDirAfterFailedCheck,
992                   Build::shared_from_this(),
993                   pkgInfo_);
994       }
995 
996       // run the source build
997       module_context::processSupervisor().runCommand(rCmd.shellCommand(),
998                                                      pkgOptions,
999                                                      buildCb);
1000    }
1001 
rExecute(const std::string & command,const FilePath & workingDir,core::system::ProcessOptions pkgOptions,bool vanilla,const core::system::ProcessCallbacks & cb)1002    bool rExecute(const std::string& command,
1003                  const FilePath& workingDir,
1004                  core::system::ProcessOptions pkgOptions,
1005                  bool vanilla,
1006                  const core::system::ProcessCallbacks& cb)
1007    {
1008       // Find the path to R
1009       FilePath rProgramPath;
1010       Error error = module_context::rScriptPath(&rProgramPath);
1011       if (error)
1012       {
1013          terminateWithError("attempting to locate R binary", error);
1014          return false;
1015       }
1016 
1017       // execute within the package directory
1018       pkgOptions.workingDir = workingDir;
1019 
1020       // build args
1021       std::vector<std::string> args;
1022       if (vanilla)
1023          args.push_back("--vanilla");
1024 
1025       args.push_back("-s");
1026       args.push_back("-e");
1027       args.push_back(command);
1028 
1029       // run it
1030       module_context::processSupervisor().runProgram(
1031                string_utils::utf8ToSystem(rProgramPath.getAbsolutePath()),
1032                args,
1033                pkgOptions,
1034                cb);
1035 
1036       return true;
1037    }
1038 
devtoolsExecute(const std::string & command,const FilePath & packagePath,core::system::ProcessOptions pkgOptions,const core::system::ProcessCallbacks & cb)1039    bool devtoolsExecute(const std::string& command,
1040                         const FilePath& packagePath,
1041                         core::system::ProcessOptions pkgOptions,
1042                         const core::system::ProcessCallbacks& cb)
1043    {
1044       if (!rExecute(command, packagePath, pkgOptions, true /* --vanilla */, cb))
1045          return false;
1046 
1047       usedDevtools_ = true;
1048       return true;
1049    }
1050 
devtoolsCheckPackage(const FilePath & packagePath,const core::system::ProcessOptions & pkgOptions,const core::system::ProcessCallbacks & cb)1051    void devtoolsCheckPackage(const FilePath& packagePath,
1052                              const core::system::ProcessOptions& pkgOptions,
1053                              const core::system::ProcessCallbacks& cb)
1054    {
1055       // build the call to check
1056       std::ostringstream ostr;
1057       ostr << "devtools::check(";
1058 
1059       std::vector<std::string> args;
1060 
1061       if (projectConfig().packageRoxygenize.empty() ||
1062           !options_.autoRoxygenizeForCheck)
1063          args.push_back("document = FALSE");
1064 
1065       if (!prefs::userPrefs().cleanupAfterRCmdCheck())
1066          args.push_back("cleanup = FALSE");
1067 
1068       // optional extra check args
1069       if (!projectConfig().packageCheckArgs.empty())
1070       {
1071          args.push_back("args = " +
1072                         packageArgsVector(projectConfig().packageCheckArgs));
1073       }
1074 
1075       // optional extra build args
1076       if (!projectConfig().packageBuildArgs.empty())
1077       {
1078          // propagate check vignette args
1079          // add --no-manual and --no-build-vignettes if they are specified
1080          std::string buildArgs = projectConfig().packageBuildArgs;
1081          std::string checkArgs = projectConfig().packageCheckArgs;
1082          if (checkArgs.find("--no-manual") != std::string::npos)
1083             buildArgs.append(" --no-manual");
1084          if (checkArgs.find("--no-build-vignettes") != std::string::npos)
1085             buildArgs.append(" --no-build-vignettes");
1086 
1087          args.push_back("build_args = " + packageArgsVector(buildArgs));
1088       }
1089 
1090       // add the args
1091       ostr << boost::algorithm::join(args, ", ");
1092 
1093       // enque the command string without the check_dir
1094       enqueCommandString(ostr.str() + ")");
1095 
1096       // now complete the command
1097       if (session::options().packageOutputInPackageFolder())
1098          ostr << ", check_dir = getwd())";
1099       else
1100          ostr << ", check_dir = dirname(getwd()))";
1101       std::string command = ostr.str();
1102 
1103       // set a success message
1104       successMessage_ = "\nR CMD check succeeded\n";
1105 
1106       // bind a success function if appropriate
1107       if (prefs::userPrefs().cleanupAfterRCmdCheck())
1108       {
1109          successFunction_ = boost::bind(&Build::cleanupAfterCheck,
1110                                         Build::shared_from_this(),
1111                                         pkgInfo_);
1112       }
1113 
1114       if (prefs::userPrefs().viewDirAfterRCmdCheck())
1115       {
1116          failureFunction_ = boost::bind(&Build::viewDirAfterFailedCheck,
1117                                         Build::shared_from_this(),
1118                                         pkgInfo_);
1119       }
1120 
1121       // run it
1122       devtoolsExecute(command, packagePath, pkgOptions, cb);
1123    }
1124 
devtoolsTestPackage(const FilePath & packagePath,const core::system::ProcessOptions & pkgOptions,const core::system::ProcessCallbacks & cb)1125    void devtoolsTestPackage(const FilePath& packagePath,
1126                             const core::system::ProcessOptions& pkgOptions,
1127                             const core::system::ProcessCallbacks& cb)
1128    {
1129       std::string command = "devtools::test()";
1130       enqueCommandString(command);
1131       devtoolsExecute(command, packagePath, pkgOptions, cb);
1132    }
1133 
testPackage(const FilePath & packagePath,core::system::ProcessOptions pkgOptions,const core::system::ProcessCallbacks & cb)1134    void testPackage(const FilePath& packagePath,
1135                     core::system::ProcessOptions pkgOptions,
1136                     const core::system::ProcessCallbacks& cb)
1137    {
1138       FilePath rScriptPath;
1139       Error error = module_context::rScriptPath(&rScriptPath);
1140       if (error)
1141       {
1142          terminateWithError("Locating R script", error);
1143          return;
1144       }
1145 
1146       // navigate to the tests directory and source all R
1147       // scripts within
1148       FilePath testsPath = packagePath.completePath("tests");
1149 
1150       // construct a shell command to execute
1151       shell_utils::ShellCommand cmd(rScriptPath);
1152       cmd << "--vanilla";
1153       cmd << "-s";
1154       cmd << "-e";
1155       std::vector<std::string> rSourceCommands;
1156 
1157       boost::format fmt(
1158          "setwd('%1%');"
1159          "files <- list.files(pattern = '[.][rR]$');"
1160          "invisible(lapply(files, function(x) {"
1161          "    system(paste(shQuote('%2%'), '--vanilla -s -f', shQuote(x)))"
1162          "}))"
1163       );
1164 
1165       cmd << boost::str(fmt %
1166                            testsPath.getAbsolutePath() %
1167                         rScriptPath.getAbsolutePath());
1168 
1169       pkgOptions.workingDir = testsPath;
1170       enqueCommandString("Sourcing R files in 'tests' directory");
1171       successMessage_ = "\nTests complete";
1172       module_context::processSupervisor().runCommand(cmd,
1173                                                      pkgOptions,
1174                                                      cb);
1175 
1176    }
1177 
testFile(const FilePath & testPath,core::system::ProcessOptions pkgOptions,const core::system::ProcessCallbacks & cb)1178    void testFile(const FilePath& testPath,
1179                  core::system::ProcessOptions pkgOptions,
1180                  const core::system::ProcessCallbacks& cb)
1181    {
1182       FilePath rScriptPath;
1183       Error error = module_context::rScriptPath(&rScriptPath);
1184       if (error)
1185       {
1186          terminateWithError("Locating R script", error);
1187          return;
1188       }
1189 
1190       // construct a shell command to execute
1191       shell_utils::ShellCommand cmd(rScriptPath);
1192       cmd << "--vanilla";
1193       cmd << "-s";
1194       cmd << "-e";
1195       std::vector<std::string> rSourceCommands;
1196 
1197       boost::format fmt(
1198          "if (nzchar('%1%')) devtools::load_all(dirname('%2%'));"
1199          "testthat::test_file('%2%')"
1200       );
1201 
1202       std::string testPathEscaped =
1203          string_utils::singleQuotedStrEscape(string_utils::utf8ToSystem(
1204             testPath.getAbsolutePath()));
1205 
1206       cmd << boost::str(fmt %
1207                         pkgInfo_.name() %
1208                         testPathEscaped);
1209 
1210       enqueCommandString("Testing R file using 'testthat'");
1211       successMessage_ = "\nTest complete";
1212       module_context::processSupervisor().runCommand(cmd,
1213                                                      pkgOptions,
1214                                                      cb);
1215 
1216    }
1217 
testShiny(FilePath & shinyPath,core::system::ProcessOptions testOptions,const core::system::ProcessCallbacks & cb,const std::string & type)1218    void testShiny(FilePath& shinyPath,
1219                   core::system::ProcessOptions testOptions,
1220                   const core::system::ProcessCallbacks& cb,
1221                   const std::string& type)
1222    {
1223       // normalize paths between all tests and single test
1224       std::string shinyTestName;
1225       if (type == kTestShinyFile) {
1226         shinyTestName = shinyPath.getFilename();
1227         shinyPath = shinyPath.getParent();
1228         if (shinyPath.getFilename() == "shinytests" ||
1229             shinyPath.getFilename() == "shinytest")
1230         {
1231            // In newer versions of shinytest, tests are stored in a "shinytest" or "shinytests"
1232            // folder under the "tests" folder.
1233            shinyPath = shinyPath.getParent();
1234         }
1235         if (shinyPath.getFilename() == "tests")
1236         {
1237            // Move up from the tests folder to the app folder.
1238            shinyPath = shinyPath.getParent();
1239         }
1240         else
1241         {
1242            // If this doesn't look like it's in a tests directory, bail out.
1243            terminateWithError("Could not find Shiny app for test in " +
1244               shinyPath.getAbsolutePath());
1245         }
1246       }
1247 
1248       // get temp path to store rds results
1249       FilePath tempPath;
1250       Error error = FilePath::tempFilePath(tempPath);
1251       if (error)
1252       {
1253          terminateWithError("Find temp dir", error);
1254          return;
1255       }
1256       error = tempPath.ensureDirectory();
1257       if (error)
1258       {
1259          terminateWithError("Creating temp dir", error);
1260          return;
1261       }
1262       FilePath tempRdsFile = tempPath.completePath(core::system::generateUuid() + ".rds");
1263 
1264       // initialize parser
1265       CompileErrorParsers parsers;
1266       parsers.add(shinytestErrorParser(shinyPath, tempRdsFile));
1267       initErrorParser(shinyPath, parsers);
1268 
1269       FilePath rScriptPath;
1270       error = module_context::rScriptPath(&rScriptPath);
1271       if (error)
1272       {
1273          terminateWithError("Locating R script", error);
1274          return;
1275       }
1276 
1277       // construct a shell command to execute
1278       shell_utils::ShellCommand cmd(rScriptPath);
1279       cmd << "--vanilla";
1280       cmd << "-s";
1281       cmd << "-e";
1282       std::vector<std::string> rSourceCommands;
1283 
1284       if (type == kTestShiny)
1285       {
1286         boost::format fmt(
1287            "result <- shinytest::testApp('%1%');"
1288            "saveRDS(result, '%2%')"
1289         );
1290 
1291         cmd << boost::str(fmt %
1292                              shinyPath.getAbsolutePath() %
1293                           tempRdsFile.getAbsolutePath());
1294       }
1295       else if (type == kTestShinyFile)
1296       {
1297         boost::format fmt(
1298            "result <- shinytest::testApp('%1%', '%2%');"
1299            "saveRDS(result, '%3%')"
1300         );
1301 
1302         cmd << boost::str(fmt %
1303                              shinyPath.getAbsolutePath() %
1304                           shinyTestName %
1305                           tempRdsFile.getAbsolutePath());
1306       }
1307       else
1308       {
1309         terminateWithError("Shiny test type is unsupported.");
1310       }
1311 
1312       enqueCommandString("Testing Shiny application using 'shinytest'");
1313       successMessage_ = "\nTest complete";
1314       module_context::processSupervisor().runCommand(cmd,
1315                                                      testOptions,
1316                                                      cb);
1317 
1318    }
1319 
devtoolsBuildPackage(const FilePath & packagePath,bool binary,const core::system::ProcessOptions & pkgOptions,const core::system::ProcessCallbacks & cb)1320    void devtoolsBuildPackage(const FilePath& packagePath,
1321                              bool binary,
1322                              const core::system::ProcessOptions& pkgOptions,
1323                              const core::system::ProcessCallbacks& cb)
1324    {
1325       // create the call to build
1326       std::ostringstream ostr;
1327       ostr << "devtools::build(";
1328 
1329       // args
1330       std::vector<std::string> args;
1331 
1332       // binary package?
1333       if (binary)
1334          args.push_back("binary = TRUE");
1335 
1336       if (session::options().packageOutputInPackageFolder())
1337          args.push_back("path = getwd()");
1338 
1339        // add R args
1340       std::string rArgs = binary ?  projectConfig().packageBuildBinaryArgs :
1341                                     projectConfig().packageBuildArgs;
1342       if (binary)
1343          rArgs.append(" --preclean");
1344       if (!rArgs.empty())
1345          args.push_back("args = " + packageArgsVector(rArgs));
1346 
1347       ostr << boost::algorithm::join(args, ", ");
1348       ostr << ")";
1349 
1350       // set a success message
1351       std::string type = binary ? "Binary" : "Source";
1352       successMessage_ = "\n" + buildPackageSuccessMsg(type);
1353 
1354       // execute it
1355       std::string command = ostr.str();
1356       enqueCommandString(command);
1357       devtoolsExecute(command, packagePath, pkgOptions, cb);
1358    }
1359 
1360 
onBuildForCheckCompleted(int exitStatus,const module_context::RCommand & checkCmd,const core::system::ProcessOptions & checkOptions,const core::system::ProcessCallbacks & checkCb)1361    void onBuildForCheckCompleted(
1362                          int exitStatus,
1363                          const module_context::RCommand& checkCmd,
1364                          const core::system::ProcessOptions& checkOptions,
1365                          const core::system::ProcessCallbacks& checkCb)
1366    {
1367       if (exitStatus == EXIT_SUCCESS)
1368       {
1369          // show the user the build command
1370          enqueCommandString(checkCmd.commandString());
1371 
1372          // run the check
1373          module_context::processSupervisor().runCommand(checkCmd.shellCommand(),
1374                                                         checkOptions,
1375                                                         checkCb);
1376       }
1377       else
1378       {
1379          terminateWithErrorStatus(exitStatus);
1380       }
1381    }
1382 
1383 
cleanupAfterCheck(const r_util::RPackageInfo & pkgInfo)1384    void cleanupAfterCheck(const r_util::RPackageInfo& pkgInfo)
1385    {
1386       // compute paths
1387       FilePath buildPath = projects::projectContext().buildTargetPath();
1388       if (!session::options().packageOutputInPackageFolder())
1389          buildPath = buildPath.getParent();
1390       FilePath srcPkgPath = buildPath.completeChildPath(pkgInfo.sourcePackageFilename());
1391       FilePath chkDirPath = buildPath.completeChildPath(pkgInfo.name() + ".Rcheck");
1392 
1393       // cleanup
1394       Error error = srcPkgPath.removeIfExists();
1395       if (error)
1396          LOG_ERROR(error);
1397       error = chkDirPath.removeIfExists();
1398       if (error)
1399          LOG_ERROR(error);
1400    }
1401 
viewDirAfterFailedCheck(const r_util::RPackageInfo & pkgInfo)1402    void viewDirAfterFailedCheck(const r_util::RPackageInfo& pkgInfo)
1403    {
1404       if (!terminationRequested_)
1405       {
1406          FilePath buildPath = projects::projectContext().buildTargetPath();
1407          if (!session::options().packageOutputInPackageFolder())
1408             buildPath = buildPath.getParent();
1409          FilePath chkDirPath = buildPath.completeChildPath(pkgInfo.name() + ".Rcheck");
1410 
1411          json::Object dataJson;
1412          dataJson["directory"] = module_context::createAliasedPath(chkDirPath);
1413          dataJson["activate"] = true;
1414          ClientEvent event(client_events::kDirectoryNavigate, dataJson);
1415 
1416          module_context::enqueClientEvent(event);
1417       }
1418    }
1419 
executeMakefileBuild(const std::string & type,const FilePath & targetPath,const core::system::ProcessOptions & options,const core::system::ProcessCallbacks & cb)1420    void executeMakefileBuild(const std::string& type,
1421                              const FilePath& targetPath,
1422                              const core::system::ProcessOptions& options,
1423                              const core::system::ProcessCallbacks& cb)
1424    {
1425       // validate that there is a Makefile file
1426       FilePath makefilePath = targetPath.completeChildPath("Makefile");
1427       if (!makefilePath.exists())
1428       {
1429          boost::format fmt ("ERROR: The build directory does "
1430                             "not contain a Makefile\n"
1431                             "so the target cannot be built.\n\n"
1432                             "Build directory: %1%\n");
1433          terminateWithError(boost::str(
1434                  fmt % module_context::createAliasedPath(targetPath)));
1435          return;
1436       }
1437 
1438       // install the gcc error parser
1439       initErrorParser(targetPath, gccErrorParser(targetPath));
1440 
1441       std::string make = "make";
1442       if (!options_.makefileArgs.empty())
1443          make += " " + options_.makefileArgs;
1444 
1445       std::string makeClean = make + " clean";
1446 
1447       std::string cmd;
1448       if (type == "build-all")
1449       {
1450          cmd = make;
1451       }
1452       else if (type == "clean-all")
1453       {
1454          cmd = makeClean;
1455       }
1456       else if (type == "rebuild-all")
1457       {
1458          cmd = shell_utils::join_and(makeClean, make);
1459       }
1460 
1461       module_context::processSupervisor().runCommand(cmd,
1462                                                      options,
1463                                                      cb);
1464    }
1465 
executeCustomBuild(const std::string &,const FilePath & customScriptPath,const core::system::ProcessOptions & options,const core::system::ProcessCallbacks & cb)1466    void executeCustomBuild(const std::string& /*type*/,
1467                            const FilePath& customScriptPath,
1468                            const core::system::ProcessOptions& options,
1469                            const core::system::ProcessCallbacks& cb)
1470    {
1471       module_context::processSupervisor().runCommand(
1472                            shell_utils::ShellCommand(customScriptPath),
1473                            options,
1474                            cb);
1475    }
1476 
executeQuartoBuild(const std::string & subType,const core::system::ProcessOptions & options,const core::system::ProcessCallbacks & cb)1477    void executeQuartoBuild(const std::string& subType,
1478                            const core::system::ProcessOptions& options,
1479                            const core::system::ProcessCallbacks& cb)
1480    {
1481       // show preview on complete
1482       successFunction_ = boost::bind(&Build::showQuartoSitePreview,
1483                                      Build::shared_from_this());
1484 
1485        auto cmd = shell_utils::ShellCommand("quarto");
1486        cmd << "render";
1487        if (!subType.empty())
1488           cmd << "--to" << subType;
1489        module_context::processSupervisor().runCommand(cmd, options,cb);
1490    }
1491 
1492 
executeWebsiteBuild(const std::string & type,const std::string & subType,const FilePath & websitePath,const core::system::ProcessOptions & options,const core::system::ProcessCallbacks & cb)1493    void executeWebsiteBuild(const std::string& type,
1494                             const std::string& subType,
1495                             const FilePath& websitePath,
1496                             const core::system::ProcessOptions& options,
1497                             const core::system::ProcessCallbacks& cb)
1498    {
1499       std::string command;
1500 
1501       if (type == "build-all")
1502       {
1503          if (options_.previewWebsite)
1504          {
1505             successFunction_ = boost::bind(&Build::showWebsitePreview,
1506                                            Build::shared_from_this(),
1507                                            websitePath);
1508          }
1509 
1510          // if there is a subType then use it to set the output format
1511          if (!subType.empty())
1512          {
1513             projects::projectContext().setWebsiteOutputFormat(subType);
1514             options_.websiteOutputFormat = subType;
1515          }
1516 
1517          boost::format fmt("rmarkdown::render_site(%1%)");
1518          std::string format;
1519          if (options_.websiteOutputFormat != "all")
1520             format = "output_format = '" + options_.websiteOutputFormat + "', ";
1521 
1522          format += ("encoding = '" +
1523                     projects::projectContext().defaultEncoding() +
1524                     "'");
1525 
1526          command = boost::str(fmt % format);
1527       }
1528       else if (type == "clean-all")
1529       {
1530          command = "rmarkdown::clean_site()";
1531       }
1532 
1533       // execute command
1534       enqueCommandString(command);
1535       rExecute(command, websitePath, options, false /* --vanilla */, cb);
1536    }
1537 
enquePreviewRmdEvent(const FilePath & sourceFile,const FilePath & outputFile)1538    void enquePreviewRmdEvent(const FilePath& sourceFile, const FilePath& outputFile)
1539    {
1540       json::Object previewRmdJson;
1541       using namespace module_context;
1542       previewRmdJson["source_file"] = createAliasedPath(sourceFile);
1543       previewRmdJson["encoding"] = projects::projectContext().config().encoding;
1544       previewRmdJson["output_file"] = createAliasedPath(outputFile);
1545       ClientEvent event(client_events::kPreviewRmd, previewRmdJson);
1546       enqueClientEvent(event);
1547    }
1548 
showWebsitePreview(const FilePath & websitePath)1549    void showWebsitePreview(const FilePath& websitePath)
1550    {
1551       // determine source file
1552       std::string output = outputAsText();
1553       FilePath sourceFile = websiteSourceFile(websitePath);
1554       if (sourceFile.isEmpty())
1555          return;
1556 
1557       // look for Output created message
1558       FilePath outputFile = module_context::extractOutputFileCreated(sourceFile.getParent(),
1559                                                                      output);
1560       if (!outputFile.isEmpty())
1561       {
1562          enquePreviewRmdEvent(sourceFile, outputFile);
1563       }
1564    }
1565 
showQuartoSitePreview()1566    void showQuartoSitePreview()
1567    {
1568       // determine source file
1569       auto config = quarto::quartoConfig();
1570       auto quartoProjectDir = module_context::resolveAliasedPath(config.project_dir);
1571       std::string output = outputAsText();
1572       FilePath sourceFile = websiteSourceFile(quartoProjectDir);
1573       if (sourceFile.isEmpty())
1574          return;
1575 
1576       // look for Output created message
1577       FilePath outputFile = module_context::extractOutputFileCreated(
1578          projects::projectContext().directory(),
1579          output
1580       );
1581       if (!outputFile.isEmpty())
1582       {
1583          // it will be html if we did a sub-project render.
1584          if (outputFile.hasExtensionLowerCase(".html"))
1585          {
1586             quarto::handleQuartoPreview(sourceFile, outputFile, output, false);
1587          }
1588          else
1589          {
1590             enquePreviewRmdEvent(sourceFile, outputFile);
1591          }
1592       }
1593    }
1594 
websiteSourceFile(const FilePath & websiteDir)1595    FilePath websiteSourceFile(const FilePath& websiteDir)
1596    {
1597       FilePath sourceFile = websiteDir.completeChildPath("index.Rmd");
1598       if (!sourceFile.exists())
1599          sourceFile = websiteDir.completeChildPath("index.rmd");
1600       if (!sourceFile.exists())
1601          sourceFile = websiteDir.completeChildPath("index.md");
1602       if (!sourceFile.exists())
1603          sourceFile = websiteDir.completeChildPath("index.qmd");
1604       if (sourceFile.exists())
1605          return sourceFile;
1606       else
1607          return FilePath();
1608    }
1609 
terminateWithErrorStatus(int exitStatus)1610    void terminateWithErrorStatus(int exitStatus)
1611    {
1612       boost::format fmt("\nExited with status %1%.\n\n");
1613       enqueBuildOutput(module_context::kCompileOutputError,
1614                        boost::str(fmt % exitStatus));
1615       enqueBuildCompleted();
1616    }
1617 
terminateWithError(const std::string & context,const Error & error)1618    void terminateWithError(const std::string& context,
1619                            const Error& error)
1620    {
1621       std::string msg = "Error " + context + ": " + error.getSummary();
1622       terminateWithError(msg);
1623    }
1624 
terminateWithError(const std::string & msg)1625    void terminateWithError(const std::string& msg)
1626    {
1627       enqueBuildOutput(module_context::kCompileOutputError, msg);
1628       enqueBuildCompleted();
1629    }
1630 
useDevtools()1631    bool useDevtools()
1632    {
1633       return projectConfig().packageUseDevtools &&
1634              module_context::isMinimumDevtoolsInstalled();
1635    }
1636 
1637 public:
1638    virtual ~Build() = default;
1639 
isRunning() const1640    bool isRunning() const { return isRunning_; }
1641 
errorsBaseDir() const1642    const std::string& errorsBaseDir() const { return errorsBaseDir_; }
errorsAsJson() const1643    const json::Array& errorsAsJson() const { return errorsJson_; }
outputAsJson() const1644    json::Array outputAsJson() const
1645    {
1646       json::Array outputJson;
1647       std::transform(output_.begin(),
1648                      output_.end(),
1649                      std::back_inserter(outputJson),
1650                      module_context::compileOutputAsJson);
1651       return outputJson;
1652    }
type() const1653    const std::string type() const { return type_; }
1654 
outputAsText()1655    std::string outputAsText()
1656    {
1657       std::string output;
1658       for (const module_context::CompileOutput& compileOutput : output_)
1659       {
1660          output.append(compileOutput.output);
1661       }
1662       return output;
1663    }
1664 
terminate()1665    void terminate()
1666    {
1667       enqueBuildOutput(module_context::kCompileOutputNormal, "\n");
1668       terminationRequested_ = true;
1669    }
1670 
1671 private:
onContinue()1672    bool onContinue()
1673    {
1674       return !terminationRequested_;
1675    }
1676 
outputWithFilter(const std::string & output)1677    void outputWithFilter(const std::string& output)
1678    {
1679       // split into lines
1680       std::vector<std::string> lines;
1681       boost::algorithm::split(lines, output,  boost::algorithm::is_any_of("\n"));
1682 
1683       // apply filter to each line
1684       size_t size = lines.size();
1685       for (size_t i = 0; i < size; i++)
1686       {
1687          // apply filter
1688          using namespace module_context;
1689          std::string line = lines.at(i);
1690          int type = errorOutputFilterFunction_(line) ?
1691                                  kCompileOutputError : kCompileOutputNormal;
1692 
1693          // add newline if this wasn't the last line
1694          if (i != (size-1))
1695             line.append("\n");
1696 
1697          // enque the output
1698          enqueBuildOutput(type, line);
1699       }
1700    }
1701 
onStandardOutput(const std::string & output)1702    void onStandardOutput(const std::string& output)
1703    {
1704       if (errorOutputFilterFunction_)
1705          outputWithFilter(output);
1706       else
1707          enqueBuildOutput(module_context::kCompileOutputNormal, output);
1708    }
1709 
onStandardError(const std::string & output)1710    void onStandardError(const std::string& output)
1711    {
1712       if (errorOutputFilterFunction_)
1713          outputWithFilter(output);
1714       else
1715          enqueBuildOutput(module_context::kCompileOutputError, output);
1716    }
1717 
onCompleted(int exitStatus)1718    void onCompleted(int exitStatus)
1719    {
1720       using namespace module_context;
1721 
1722       // call the error parser if one has been specified
1723       if (errorParser_)
1724       {
1725          std::vector<SourceMarker> errors = errorParser_(outputAsText());
1726          if (!errors.empty())
1727          {
1728             errorsJson_ = sourceMarkersAsJson(errors);
1729             enqueBuildErrors(errorsJson_);
1730          }
1731       }
1732 
1733       if (exitStatus != EXIT_SUCCESS)
1734       {
1735          boost::format fmt("\nExited with status %1%.\n\n");
1736          enqueBuildOutput(kCompileOutputError, boost::str(fmt % exitStatus));
1737 
1738          // if this is a package build then check for ability to
1739          // build C++ code at all
1740          if (!pkgInfo_.empty() && !module_context::canBuildCpp())
1741          {
1742             // prompted install of Rtools on Windows (but don't prompt if
1743             // we used devtools since it likely has it's own prompt)
1744 #ifdef _WIN32
1745             if (!usedDevtools_)
1746                module_context::installRBuildTools("Building R packages");
1747 #endif
1748          }
1749 
1750          // if this is a package build then try to clean up a left
1751          // behind 00LOCK directory. note that R uses the directory name
1752          // and not the actual package name for the lockfile (and these can
1753          // and do differ in some cases)
1754          if (!pkgInfo_.empty() && !libPaths_.empty())
1755          {
1756             std::string pkgFolder = projects::projectContext().buildTargetPath().getFilename();
1757             FilePath libPath = libPaths_[0];
1758             FilePath lockPath = libPath.completeChildPath("00LOCK-" + pkgFolder);
1759             lockPath.removeIfExists();
1760          }
1761 
1762          // never restart R after a failed build
1763          restartR_ = false;
1764 
1765          // take other actions
1766          if (failureFunction_)
1767             failureFunction_();
1768       }
1769       else
1770       {
1771          if (!successMessage_.empty())
1772             enqueBuildOutput(kCompileOutputNormal, successMessage_ + "\n");
1773 
1774          if (successFunction_)
1775             successFunction_();
1776       }
1777 
1778       enqueBuildCompleted();
1779    }
1780 
enqueBuildOutput(int type,const std::string & output)1781    void enqueBuildOutput(int type, const std::string& output)
1782    {
1783       module_context::CompileOutput compileOutput(type, output);
1784 
1785       output_.push_back(compileOutput);
1786 
1787       ClientEvent event(client_events::kBuildOutput,
1788                         compileOutputAsJson(compileOutput));
1789 
1790       module_context::enqueClientEvent(event);
1791    }
1792 
enqueCommandString(const std::string & cmd)1793    void enqueCommandString(const std::string& cmd)
1794    {
1795       enqueBuildOutput(module_context::kCompileOutputCommand,
1796                        "==> " + cmd + "\n\n");
1797    }
1798 
enqueBuildErrors(const json::Array & errors)1799    void enqueBuildErrors(const json::Array& errors)
1800    {
1801       json::Object jsonData;
1802       jsonData["base_dir"] = errorsBaseDir_;
1803       jsonData["errors"] = errors;
1804       jsonData["open_error_list"] = openErrorList_;
1805       jsonData["type"] = type_;
1806 
1807       ClientEvent event(client_events::kBuildErrors, jsonData);
1808       module_context::enqueClientEvent(event);
1809    }
1810 
parseLibrarySwitchFromInstallArgs()1811    std::string parseLibrarySwitchFromInstallArgs()
1812    {
1813       std::string libPath;
1814 
1815       std::string extraArgs = projectConfig().packageInstallArgs;
1816       std::size_t n = extraArgs.size();
1817       std::size_t index = extraArgs.find("--library=");
1818 
1819       if (index != std::string::npos &&
1820           index < n - 2) // ensure some space for path
1821       {
1822          std::size_t startIndex = index + std::string("--library=").length();
1823          std::size_t endIndex = startIndex + 1;
1824 
1825          // The library path can be specified with quotes + spaces, or without
1826          // quotes (but no spaces), so handle both cases.
1827          char firstChar = extraArgs[startIndex];
1828          if (firstChar == '\'' || firstChar == '\"')
1829          {
1830             while (++endIndex < n)
1831             {
1832                // skip escaped characters
1833                if (extraArgs[endIndex] == '\\')
1834                {
1835                   ++endIndex;
1836                   continue;
1837                }
1838 
1839                if (extraArgs[endIndex] == firstChar)
1840                   break;
1841             }
1842 
1843             libPath = extraArgs.substr(startIndex + 1, endIndex - startIndex - 1);
1844          }
1845          else
1846          {
1847             while (++endIndex < n)
1848             {
1849                if (isspace(extraArgs[endIndex]))
1850                   break;
1851             }
1852             libPath = extraArgs.substr(startIndex, endIndex - startIndex + 1);
1853          }
1854       }
1855       return libPath;
1856    }
1857 
enqueBuildCompleted()1858    void enqueBuildCompleted()
1859    {
1860       isRunning_ = false;
1861 
1862       if (!buildToolsWarning_.empty())
1863       {
1864          enqueBuildOutput(module_context::kCompileOutputError,
1865                           buildToolsWarning_ + "\n\n");
1866       }
1867 
1868       // enque event
1869       std::string afterRestartCommand;
1870       if (restartR_)
1871       {
1872          afterRestartCommand = "library(" + pkgInfo_.name();
1873 
1874          // if --library="" was specified and we're not in devmode,
1875          // use it
1876          if (!(r::session::utils::isPackratModeOn() ||
1877                r::session::utils::isDevtoolsDevModeOn()))
1878          {
1879             std::string libPath = parseLibrarySwitchFromInstallArgs();
1880             if (!libPath.empty())
1881                afterRestartCommand += ", lib.loc = \"" + libPath + "\"";
1882          }
1883 
1884          afterRestartCommand += ")";
1885       }
1886       json::Object dataJson;
1887       dataJson["restart_r"] = restartR_;
1888       dataJson["after_restart_command"] = afterRestartCommand;
1889       ClientEvent event(client_events::kBuildCompleted, dataJson);
1890       module_context::enqueClientEvent(event);
1891    }
1892 
projectConfig()1893    const r_util::RProjectConfig& projectConfig()
1894    {
1895       return projects::projectContext().config();
1896    }
1897 
buildPackageSuccessMsg(const std::string & type)1898    std::string buildPackageSuccessMsg(const std::string& type)
1899    {
1900       FilePath writtenPath = projects::projectContext().buildTargetPath();
1901       if (!session::options().packageOutputInPackageFolder())
1902          writtenPath = writtenPath.getParent();
1903       std::string written = module_context::createAliasedPath(writtenPath);
1904       if (written == "~")
1905          written = writtenPath.getAbsolutePath();
1906 
1907       return type + " package written to " + written;
1908    }
1909 
initErrorParser(const FilePath & baseDir,CompileErrorParser parser)1910    void initErrorParser(const FilePath& baseDir, CompileErrorParser parser)
1911    {
1912       // set base dir -- make sure it ends with a / so the slash is
1913       // excluded from error display
1914       errorsBaseDir_ = module_context::createAliasedPath(baseDir);
1915       if (!errorsBaseDir_.empty() &&
1916           !boost::algorithm::ends_with(errorsBaseDir_, "/"))
1917       {
1918          errorsBaseDir_.append("/");
1919       }
1920 
1921       errorParser_ = parser;
1922    }
1923 
1924 private:
1925    bool isRunning_;
1926    bool terminationRequested_;
1927    std::vector<module_context::CompileOutput> output_;
1928    CompileErrorParser errorParser_;
1929    std::string errorsBaseDir_;
1930    json::Array errorsJson_;
1931    r_util::RPackageInfo pkgInfo_;
1932    projects::RProjectBuildOptions options_;
1933    std::vector<FilePath> libPaths_;
1934    std::string successMessage_;
1935    std::string buildToolsWarning_;
1936    boost::function<void()> successFunction_;
1937    boost::function<void()> failureFunction_;
1938    boost::function<bool(const std::string&)> errorOutputFilterFunction_;
1939    bool restartR_;
1940    bool usedDevtools_;
1941    bool openErrorList_;
1942    std::string type_;
1943 };
1944 
1945 boost::shared_ptr<Build> s_pBuild;
1946 
1947 
isBuildRunning()1948 bool isBuildRunning()
1949 {
1950    return s_pBuild && s_pBuild->isRunning();
1951 }
1952 
startBuild(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)1953 Error startBuild(const json::JsonRpcRequest& request,
1954                  json::JsonRpcResponse* pResponse)
1955 {
1956    // get type
1957    std::string type, subType;
1958    Error error = json::readParams(request.params, &type, &subType);
1959    if (error)
1960       return error;
1961 
1962    // if we have a build already running then just return false
1963    if (isBuildRunning())
1964    {
1965       pResponse->setResult(false);
1966    }
1967    else
1968    {
1969       s_pBuild = Build::create(type, subType);
1970       pResponse->setResult(true);
1971    }
1972 
1973    return Success();
1974 }
1975 
1976 
1977 
terminateBuild(const json::JsonRpcRequest &,json::JsonRpcResponse * pResponse)1978 Error terminateBuild(const json::JsonRpcRequest& /*request*/,
1979                      json::JsonRpcResponse* pResponse)
1980 {
1981    if (isBuildRunning())
1982       s_pBuild->terminate();
1983 
1984    pResponse->setResult(true);
1985 
1986    return Success();
1987 }
1988 
getCppCapabilities(const json::JsonRpcRequest &,json::JsonRpcResponse * pResponse)1989 Error getCppCapabilities(const json::JsonRpcRequest& /*request*/,
1990                          json::JsonRpcResponse* pResponse)
1991 {
1992    json::Object capsJson;
1993    capsJson["can_build"] = module_context::canBuildCpp();
1994    capsJson["can_source_cpp"] = module_context::haveRcppAttributes();
1995    pResponse->setResult(capsJson);
1996 
1997    return Success();
1998 }
1999 
installBuildTools(const json::JsonRpcRequest & request,json::JsonRpcResponse * pResponse)2000 Error installBuildTools(const json::JsonRpcRequest& request,
2001                         json::JsonRpcResponse* pResponse)
2002 {
2003    // get param
2004    std::string action;
2005    Error error = json::readParam(request.params, 0, &action);
2006    if (error)
2007       return error;
2008 
2009    pResponse->setResult(module_context::installRBuildTools(action));
2010 
2011    return Success();
2012 }
2013 
devtoolsLoadAllPath(const json::JsonRpcRequest &,json::JsonRpcResponse * pResponse)2014 Error devtoolsLoadAllPath(const json::JsonRpcRequest& /*request*/,
2015                      json::JsonRpcResponse* pResponse)
2016 {
2017    pResponse->setResult(module_context::pathRelativeTo(
2018             module_context::safeCurrentPath(),
2019             projects::projectContext().buildTargetPath()));
2020 
2021    return Success();
2022 }
2023 
2024 
2025 struct BuildContext
2026 {
emptyrstudio::session::modules::build::__anon1bd5f4b40211::BuildContext2027    bool empty() const { return errors.isEmpty() && outputs.isEmpty(); }
2028    std::string errorsBaseDir;
2029    json::Array errors;
2030    json::Array outputs;
2031    std::string type;
2032 };
2033 
2034 BuildContext s_suspendBuildContext;
2035 
2036 
writeBuildContext(const BuildContext & buildContext,core::Settings * pSettings)2037 void writeBuildContext(const BuildContext& buildContext,
2038                        core::Settings* pSettings)
2039 {
2040    pSettings->set("build-last-outputs", buildContext.outputs.write());
2041    pSettings->set("build-last-errors", buildContext.errors.write());
2042    pSettings->set("build-last-errors-base-dir", buildContext.errorsBaseDir);
2043 }
2044 
onSuspend(core::Settings * pSettings)2045 void onSuspend(core::Settings* pSettings)
2046 {
2047    if (s_pBuild)
2048    {
2049       BuildContext buildContext;
2050       buildContext.outputs = s_pBuild->outputAsJson();
2051       buildContext.errors = s_pBuild->errorsAsJson();
2052       buildContext.errorsBaseDir = s_pBuild->errorsBaseDir();
2053       buildContext.type = s_pBuild->type();
2054       writeBuildContext(buildContext, pSettings);
2055    }
2056    else if (!s_suspendBuildContext.empty())
2057    {
2058       writeBuildContext(s_suspendBuildContext, pSettings);
2059    }
2060    else
2061    {
2062       BuildContext emptyBuildContext;
2063       writeBuildContext(emptyBuildContext, pSettings);
2064    }
2065 }
2066 
onResume(const core::Settings & settings)2067 void onResume(const core::Settings& settings)
2068 {
2069    std::string buildLastOutputs = settings.get("build-last-outputs");
2070    if (!buildLastOutputs.empty())
2071    {
2072       json::Value outputsJson;
2073       if (!outputsJson.parse(buildLastOutputs) &&
2074           json::isType<json::Array>(outputsJson))
2075       {
2076          s_suspendBuildContext.outputs = outputsJson.getValue<json::Array>();
2077       }
2078    }
2079 
2080    s_suspendBuildContext.errorsBaseDir = settings.get("build-last-errors-base-dir");
2081    std::string buildLastErrors = settings.get("build-last-errors");
2082    if (!buildLastErrors.empty())
2083    {
2084       json::Value errorsJson;
2085       if (!errorsJson.parse(buildLastErrors) &&
2086           json::isType<json::Array>(errorsJson))
2087       {
2088          s_suspendBuildContext.errors = errorsJson.getValue<json::Array>();
2089       }
2090    }
2091 }
2092 
2093 
rs_canBuildCpp()2094 SEXP rs_canBuildCpp()
2095 {
2096    r::sexp::Protect rProtect;
2097    return r::sexp::create(module_context::canBuildCpp(), &rProtect);
2098 }
2099 
2100 std::string s_previousPath;
rs_restorePreviousPath()2101 SEXP rs_restorePreviousPath()
2102 {
2103 #ifdef _WIN32
2104     if (!s_previousPath.empty())
2105         core::system::setenv("PATH", s_previousPath);
2106     s_previousPath.clear();
2107 #endif
2108     return R_NilValue;
2109 }
2110 
rs_addRToolsToPath()2111 SEXP rs_addRToolsToPath()
2112 {
2113 #ifdef _WIN32
2114     s_previousPath = core::system::getenv("PATH");
2115     std::string newPath = s_previousPath;
2116     std::string warningMsg;
2117     bool result = module_context::addRtoolsToPathIfNecessary(&newPath, &warningMsg);
2118     if (!warningMsg.empty())
2119        REprintf("%s\n", warningMsg.c_str());
2120     core::system::setenv("PATH", newPath);
2121     r::sexp::Protect protect;
2122     return r::sexp::create(result, &protect);
2123 #endif
2124     return R_NilValue;
2125 }
2126 
2127 #ifdef _WIN32
2128 
rs_installBuildTools()2129 SEXP rs_installBuildTools()
2130 {
2131    Error error = installRtools();
2132    if (error)
2133       LOG_ERROR(error);
2134 
2135    return R_NilValue;
2136 }
2137 
2138 #elif __APPLE__
2139 
rs_installBuildTools()2140 SEXP rs_installBuildTools()
2141 {
2142    if (module_context::isMacOS())
2143    {
2144       if (!module_context::hasMacOSCommandLineTools())
2145       {
2146          core::system::ProcessResult result;
2147          Error error = core::system::runCommand(
2148                   "/usr/bin/xcode-select --install",
2149                   core::system::ProcessOptions(),
2150                   &result);
2151          if (error)
2152             LOG_ERROR(error);
2153       }
2154    }
2155    else
2156    {
2157       ClientEvent event = browseUrlEvent(
2158           "https://www.rstudio.org/links/install_osx_build_tools");
2159       module_context::enqueClientEvent(event);
2160    }
2161    return R_NilValue;
2162 }
2163 
2164 #else
2165 
rs_installBuildTools()2166 SEXP rs_installBuildTools()
2167 {
2168    return R_NilValue;
2169 }
2170 
2171 #endif
2172 
2173 
rs_installPackage(SEXP pkgPathSEXP,SEXP libPathSEXP)2174 SEXP rs_installPackage(SEXP pkgPathSEXP, SEXP libPathSEXP)
2175 {
2176    using namespace rstudio::r::sexp;
2177    Error error = module_context::installPackage(safeAsString(pkgPathSEXP),
2178                                                 safeAsString(libPathSEXP));
2179    if (error)
2180    {
2181       std::string desc = error.getProperty("description");
2182       if (!desc.empty())
2183          module_context::consoleWriteError(desc + "\n");
2184       LOG_ERROR(error);
2185    }
2186 
2187    return R_NilValue;
2188 }
2189 
getBookdownFormats(const json::JsonRpcRequest &,json::JsonRpcResponse * pResponse)2190 Error getBookdownFormats(const json::JsonRpcRequest& /*request*/,
2191                          json::JsonRpcResponse* pResponse)
2192 {
2193    json::Object responseJson;
2194    responseJson["output_format"] = projects::projectContext().buildOptions().websiteOutputFormat;
2195    responseJson["website_output_formats"] = projects::websiteOutputFormatsJson();
2196    pResponse->setResult(responseJson);
2197    return Success();
2198 }
2199 
2200 
2201 
2202 } // anonymous namespace
2203 
buildStateAsJson()2204 json::Value buildStateAsJson()
2205 {
2206    if (s_pBuild)
2207    {
2208       json::Object stateJson;
2209       stateJson["running"] = s_pBuild->isRunning();
2210       stateJson["outputs"] = s_pBuild->outputAsJson();
2211       stateJson["errors_base_dir"] = s_pBuild->errorsBaseDir();
2212       stateJson["type"] = s_pBuild->type();
2213       stateJson["errors"] = s_pBuild->errorsAsJson();
2214       return std::move(stateJson);
2215    }
2216    else if (!s_suspendBuildContext.empty())
2217    {
2218       json::Object stateJson;
2219       stateJson["running"] = false;
2220       stateJson["outputs"] = s_suspendBuildContext.outputs;
2221       stateJson["errors_base_dir"] = s_suspendBuildContext.errorsBaseDir;
2222       stateJson["type"] = s_suspendBuildContext.type;
2223       stateJson["errors"] = s_suspendBuildContext.errors;
2224       return std::move(stateJson);
2225    }
2226    else
2227    {
2228       return json::Value();
2229    }
2230 }
2231 
2232 namespace {
2233 
manageUserMakevars()2234 void manageUserMakevars()
2235 {
2236    // NOTE: previously, when Apple machines were transitioning from the
2237    // use of gcc to clang, we wrote a custom ~/.R/Makevars file for users
2238    // that would ensure R uses clang during compilation. unfortunately,
2239    // this causes issues with newer releases of R as those releases will
2240    // encode extra flags (e.g. the default C++ standard) directly into
2241    // the CXX make variable. to recover, we now remove ~/.R/Makevars if
2242    // it exists and contains only the stubs that we added
2243    // https://github.com/rstudio/rstudio/issues/8800
2244    // to clang if necessary
2245    using namespace module_context;
2246 
2247    // nothing to do on non-macOS platforms
2248    if (!isMacOS())
2249       return;
2250 
2251    // check for existing ~/.R/Makevars file
2252    FilePath makevarsPath = userHomePath().completeChildPath(".R/Makevars");
2253    if (makevarsPath.exists())
2254    {
2255       std::string contents;
2256       Error error = core::readStringFromFile(makevarsPath, &contents);
2257       if (error)
2258          LOG_ERROR(error);
2259 
2260       // trim whitespace
2261       contents = core::string_utils::trimWhitespace(contents);
2262 
2263       // if this is the old stub generated by RStudio, remove it
2264       std::string makevars = "CC=clang\nCXX=clang++";
2265       if (contents == makevars)
2266       {
2267          error = makevarsPath.remove();
2268          if (error)
2269             LOG_ERROR(error);
2270       }
2271    }
2272 
2273 }
2274 
2275 } // end anonymous namespace
2276 
onDeferredInit(bool newSession)2277 void onDeferredInit(bool newSession)
2278 {
2279    manageUserMakevars();
2280 }
2281 
initialize()2282 Error initialize()
2283 {
2284    // register .Call methods
2285    RS_REGISTER_CALL_METHOD(rs_canBuildCpp);
2286    RS_REGISTER_CALL_METHOD(rs_addRToolsToPath);
2287    RS_REGISTER_CALL_METHOD(rs_restorePreviousPath);
2288    RS_REGISTER_CALL_METHOD(rs_installPackage);
2289    RS_REGISTER_CALL_METHOD(rs_installBuildTools);
2290 
2291    // subscribe to deferredInit for build tools fixup
2292    module_context::events().onDeferredInit.connect(onDeferredInit);
2293 
2294    // subscribe to file monitor and source editor file saved so we
2295    // can tickle a flag to indicates when we should force an R
2296    // package rebuild
2297    session::projects::FileMonitorCallbacks cb;
2298    cb.onFilesChanged = onFilesChanged;
2299    projects::projectContext().subscribeToFileMonitor("", cb);
2300    module_context::events().onSourceEditorFileSaved.connect(onSourceEditorFileSaved);
2301 
2302    // add suspend handler
2303    addSuspendHandler(module_context::SuspendHandler(boost::bind(onSuspend, _2),
2304                                                     onResume));
2305 
2306    // install rpc methods
2307    using boost::bind;
2308    using namespace module_context;
2309    ExecBlock initBlock;
2310    initBlock.addFunctions()
2311       (bind(registerRpcMethod, "start_build", startBuild))
2312       (bind(registerRpcMethod, "terminate_build", terminateBuild))
2313       (bind(registerRpcMethod, "get_cpp_capabilities", getCppCapabilities))
2314       (bind(registerRpcMethod, "install_build_tools", installBuildTools))
2315       (bind(registerRpcMethod, "devtools_load_all_path", devtoolsLoadAllPath))
2316       (bind(registerRpcMethod, "get_bookdown_formats", getBookdownFormats))
2317       (bind(sourceModuleRFile, "SessionBuild.R"))
2318       (bind(source_cpp::initialize));
2319    return initBlock.execute();
2320 }
2321 
2322 
2323 } // namespace build
2324 } // namespace modules
2325 
2326 namespace module_context {
2327 
2328 #ifdef __APPLE__
2329 namespace {
2330 
usingSystemMake()2331 bool usingSystemMake()
2332 {
2333    return findProgram("make").getAbsolutePath() == "/usr/bin/make";
2334 }
2335 
2336 } // anonymous namespace
2337 #endif
2338 
haveRcppAttributes()2339 bool haveRcppAttributes()
2340 {
2341    return module_context::isPackageVersionInstalled("Rcpp", "0.10.1");
2342 }
2343 
canBuildCpp()2344 bool canBuildCpp()
2345 {
2346    if (s_canBuildCpp)
2347       return true;
2348 
2349 #ifdef __APPLE__
2350    // NOTE: on macOS, R normally requests user install and use its own
2351    // LLVM toolchain; however, that toolchain still needs to re-use
2352    // system headers provided by the default macOS toolchain, and so
2353    // we still want to check for macOS command line tools here
2354    if (isMacOS() &&
2355        usingSystemMake() &&
2356        !hasMacOSCommandLineTools())
2357    {
2358       return false;
2359    }
2360 #endif
2361 
2362    // try to build a simple c file to test whether we have build tools available
2363    FilePath cppPath = module_context::tempFile("test", "c");
2364    Error error = core::writeStringToFile(cppPath, "void test() {}\n");
2365    if (error)
2366    {
2367       LOG_ERROR(error);
2368       return false;
2369    }
2370 
2371    // get R bin directory
2372    FilePath rBinDir;
2373    error = module_context::rBinDir(&rBinDir);
2374    if (error)
2375    {
2376       LOG_ERROR(error);
2377       return false;
2378    }
2379 
2380    // try to run build tools
2381    RCommand rCmd(rBinDir);
2382    rCmd << "SHLIB";
2383    rCmd << cppPath.getFilename();
2384 
2385    core::system::ProcessOptions options;
2386    options.workingDir = cppPath.getParent();
2387    core::system::Options childEnv;
2388    core::system::environment(&childEnv);
2389    std::string warningMsg;
2390    module_context::addRtoolsToPathIfNecessary(&childEnv, &warningMsg);
2391    options.environment = childEnv;
2392 
2393    core::system::ProcessResult result;
2394    error = core::system::runCommand(rCmd.shellCommand(), options, &result);
2395    if (error)
2396    {
2397       LOG_ERROR(error);
2398       return false;
2399    }
2400 
2401    if (result.exitStatus != EXIT_SUCCESS)
2402    {
2403       checkXcodeLicense();
2404       return false;
2405    }
2406 
2407    s_canBuildCpp = true;
2408    return true;
2409 }
2410 
installRBuildTools(const std::string & action)2411 bool installRBuildTools(const std::string& action)
2412 {
2413 #if defined(_WIN32) || defined(__APPLE__)
2414    r::exec::RFunction check(".rs.installBuildTools", action);
2415    bool userConfirmed = false;
2416    Error error = check.call(&userConfirmed);
2417    if (error)
2418       LOG_ERROR(error);
2419    return userConfirmed;
2420 #else
2421    return false;
2422 #endif
2423 }
2424 
2425 }
2426 
2427 } // namespace session
2428 } // namespace rstudio
2429 
2430