1 /*
2 * SessionSourceCpp.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 "SessionSourceCpp.hpp"
17
18 #include <boost/algorithm/string/trim.hpp>
19 #include <boost/algorithm/string/predicate.hpp>
20 #include <boost/algorithm/string/join.hpp>
21
22 #include <core/BoostSignals.hpp>
23 #include <shared_core/Error.hpp>
24 #include <shared_core/FilePath.hpp>
25 #include <core/StringUtils.hpp>
26
27 #include <r/RSexp.hpp>
28 #include <r/RRoutines.hpp>
29
30 #include <session/SessionModuleContext.hpp>
31
32 #include "SessionBuildErrors.hpp"
33
34 using namespace rstudio::core;
35
36 namespace rstudio {
37 namespace session {
38 namespace modules {
39 namespace build {
40 namespace source_cpp {
41
42 namespace {
43
44 struct SourceCppState
45 {
emptyrstudio::session::modules::build::source_cpp::__anon711826780111::SourceCppState46 bool empty() const { return errors.isEmpty() && outputs.isEmpty(); }
47
clearrstudio::session::modules::build::source_cpp::__anon711826780111::SourceCppState48 void clear()
49 {
50 targetFile.clear();
51 errors.clear();
52 outputs.clear();
53 }
54
addOutputrstudio::session::modules::build::source_cpp::__anon711826780111::SourceCppState55 void addOutput(int type, const std::string& output)
56 {
57 using namespace module_context;
58 outputs.push_back(compileOutputAsJson(CompileOutput(type,output)));
59 }
60
asJsonrstudio::session::modules::build::source_cpp::__anon711826780111::SourceCppState61 json::Value asJson() const
62 {
63 json::Object stateJson;
64 stateJson["target_file"] = targetFile;
65 stateJson["outputs"] = outputs;
66 stateJson["errors"] = errors;
67 return std::move(stateJson);
68 }
69
70 std::string targetFile;
71 json::Array errors;
72 json::Array outputs;
73 };
74
enqueSourceCppStarted()75 void enqueSourceCppStarted()
76 {
77 ClientEvent event(client_events::kSourceCppStarted);
78 module_context::enqueClientEvent(event);
79 }
80
enqueSourceCppCompleted(const FilePath & sourceFile,const std::string & output,const std::string & errorOutput)81 void enqueSourceCppCompleted(const FilePath& sourceFile,
82 const std::string& output,
83 const std::string& errorOutput)
84 {
85 // reset last sourceCpp state with new data
86 using namespace module_context;
87 SourceCppState sourceCppState;
88 sourceCppState.targetFile = module_context::createAliasedPath(sourceFile);
89 sourceCppState.addOutput(kCompileOutputNormal, output);
90 sourceCppState.addOutput(kCompileOutputError, errorOutput);
91
92 // parse errors
93 std::string allOutput = output + "\n" + errorOutput;
94 CompileErrorParser errorParser = gccErrorParser(sourceFile.getParent());
95 std::vector<SourceMarker> errors = errorParser(allOutput);
96 sourceCppState.errors = sourceMarkersAsJson(errors);
97
98 // enque event
99 ClientEvent event(client_events::kSourceCppCompleted,
100 sourceCppState.asJson());
101 module_context::enqueClientEvent(event);
102 }
103
104
105 class SourceCppContext : boost::noncopyable
106 {
107 private:
SourceCppContext()108 SourceCppContext() {}
109 friend SourceCppContext& sourceCppContext();
110
111 public:
onBuild(const FilePath & sourceFile,bool fromCode,bool showOutput)112 bool onBuild(const FilePath& sourceFile, bool fromCode, bool showOutput)
113 {
114 // always clear state before starting a new build
115 reset();
116
117 // capture params
118 sourceFile_ = sourceFile;
119 fromCode_ = fromCode;
120 showOutput_ = showOutput;
121
122 // fixup path if necessary
123 std::string path = core::system::getenv("PATH");
124 std::string newPath = path;
125 if (module_context::addRtoolsToPathIfNecessary(&newPath, &rToolsWarning_))
126 {
127 previousPath_ = path;
128 core::system::setenv("PATH", newPath);
129 }
130
131 // capture all output that goes to the console
132 module_context::events().onConsoleOutput.connect(
133 boost::bind(&SourceCppContext::onConsoleOutput, this, _1, _2));
134
135 // enque build started
136 enqueSourceCppStarted();
137
138 // return true to indicate it's okay to build
139 return true;
140 }
141
onBuildComplete(bool succeeded,const std::string & output)142 void onBuildComplete(bool succeeded, const std::string& output)
143 {
144 // defer handling of build complete so we make sure to get all of the
145 // stderr output from console std stream capture
146 module_context::scheduleDelayedWork(
147 boost::posix_time::milliseconds(200),
148 boost::bind(&SourceCppContext::handleBuildComplete,
149 this, succeeded, output),
150 true); // idle only
151 }
152
153 private:
154
handleBuildComplete(bool succeeded,const std::string & output)155 void handleBuildComplete(bool succeeded, const std::string& output)
156 {
157 // restore previous path
158 if (!previousPath_.empty())
159 core::system::setenv("PATH", previousPath_);
160
161 // collect all build output (do this before r tools warning so
162 // it's output doesn't end up in consoleErrorBuffer_)
163 std::string buildOutput;
164 if (!succeeded || showOutput_)
165 buildOutput = consoleOutputBuffer_;
166 else
167 buildOutput = output;
168
169 // if we failed and there was an R tools warning then show it
170 if (!succeeded)
171 {
172 if (!rToolsWarning_.empty())
173 module_context::consoleWriteError(rToolsWarning_);
174
175 // prompted install of Rtools on Win32
176 #ifdef _WIN32
177 if (!module_context::canBuildCpp())
178 module_context::installRBuildTools("Compiling C/C++ code for R");
179 #endif
180 }
181
182 // parse for gcc errors for sourceCpp
183 if (!fromCode_)
184 enqueSourceCppCompleted(sourceFile_, buildOutput, consoleErrorBuffer_);
185
186 // reset state
187 reset();
188 }
189
190
onConsoleOutput(module_context::ConsoleOutputType type,std::string output)191 void onConsoleOutput(module_context::ConsoleOutputType type,
192 std::string output)
193 {
194 #ifdef _WIN32
195 // on windows make sure that output ends with a newline (because
196 // standard output and error both come in on the same channel not
197 // separated by newlines which prevents us from parsing errors)
198 if (!boost::algorithm::ends_with(output, "\n"))
199 output += "\n";
200 #endif
201
202 if (type == module_context::ConsoleOutputNormal)
203 consoleOutputBuffer_.append(output);
204 else
205 consoleErrorBuffer_.append(output);
206 }
207
reset()208 void reset()
209 {
210 sourceFile_ = FilePath();
211 showOutput_ = false;
212 fromCode_ = false;
213 consoleOutputBuffer_.clear();
214 consoleErrorBuffer_.clear();
215 module_context::events().onConsoleOutput.disconnect(
216 boost::bind(&SourceCppContext::onConsoleOutput, this, _1, _2));
217 previousPath_.clear();
218 rToolsWarning_.clear();
219 }
220
221 private:
222 FilePath sourceFile_;
223 bool showOutput_;
224 bool fromCode_;
225 std::string consoleOutputBuffer_;
226 std::string consoleErrorBuffer_;
227 std::string previousPath_;
228 std::string rToolsWarning_;
229 };
230
sourceCppContext()231 SourceCppContext& sourceCppContext()
232 {
233 static SourceCppContext instance;
234 return instance;
235 }
236
237
238
rs_sourceCppOnBuild(SEXP sFile,SEXP sFromCode,SEXP sShowOutput)239 SEXP rs_sourceCppOnBuild(SEXP sFile, SEXP sFromCode, SEXP sShowOutput)
240 {
241 std::string file = r::sexp::asString(sFile);
242 FilePath filePath(string_utils::systemToUtf8(file));
243 bool fromCode = r::sexp::asLogical(sFromCode);
244 bool showOutput = r::sexp::asLogical(sShowOutput);
245
246 bool doBuild = sourceCppContext().onBuild(filePath, fromCode, showOutput);
247
248 r::sexp::Protect rProtect;
249 return r::sexp::create(doBuild, &rProtect);
250 }
251
rs_sourceCppOnBuildComplete(SEXP sSucceeded,SEXP sOutput)252 SEXP rs_sourceCppOnBuildComplete(SEXP sSucceeded, SEXP sOutput)
253 {
254 bool succeeded = r::sexp::asLogical(sSucceeded);
255
256 std::string output;
257 if (sOutput != R_NilValue)
258 {
259 std::vector<std::string> outputLines;
260 Error error = r::sexp::extract(sOutput, &outputLines);
261 if (error)
262 LOG_ERROR(error);
263 output = boost::algorithm::join(outputLines, "\n");
264 }
265
266 sourceCppContext().onBuildComplete(succeeded, output);
267
268 return R_NilValue;
269 }
270
271
272 } // anonymous namespace
273
274
initialize()275 Error initialize()
276 {
277 RS_REGISTER_CALL_METHOD(rs_sourceCppOnBuild);
278 RS_REGISTER_CALL_METHOD(rs_sourceCppOnBuildComplete);
279 return Success();
280 }
281
282 } // namespace source_cpp
283 } // namespace build
284 } // namespace modules
285 } // namespace session
286 } // namespace rstudio
287