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