1 /*
2 * SessionCompilePdf.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 "SessionCompilePdf.hpp"
17
18 #include <set>
19
20 #include <boost/format.hpp>
21 #include <boost/enable_shared_from_this.hpp>
22
23 #include <shared_core/FilePath.hpp>
24 #include <core/Exec.hpp>
25 #include <core/Settings.hpp>
26 #include <core/Algorithm.hpp>
27 #include <core/FileSerializer.hpp>
28 #include <core/system/ShellUtils.hpp>
29
30 #include <core/tex/TexLogParser.hpp>
31 #include <core/tex/TexMagicComment.hpp>
32
33 #include <r/RSexp.hpp>
34 #include <r/RExec.hpp>
35 #include <r/RRoutines.hpp>
36
37 #include <session/SessionModuleContext.hpp>
38 #include <session/projects/SessionProjects.hpp>
39 #include <session/prefs/UserPrefs.hpp>
40
41 #include "SessionPdfLatex.hpp"
42 #include "SessionRnwWeave.hpp"
43 #include "SessionRnwConcordance.hpp"
44 #include "SessionSynctex.hpp"
45 #include "SessionCompilePdfSupervisor.hpp"
46 #include "SessionViewPdf.hpp"
47 #include "SessionTexUtils.hpp"
48
49 using namespace rstudio::core;
50
51 namespace rstudio {
52 namespace session {
53 namespace modules {
54 namespace tex {
55 namespace compile_pdf {
56
57 namespace {
58
59 // track compile pdf state so we can send it to the client on client init
60 class CompilePdfState : boost::noncopyable
61 {
62 public:
CompilePdfState()63 CompilePdfState()
64 : tabVisible_(false), running_(false)
65 {
66 }
67
~CompilePdfState()68 virtual ~CompilePdfState() {}
69 // COPYING: noncoypable
70
readFromJson(const json::Object & asJson)71 Error readFromJson(const json::Object& asJson)
72 {
73 clear();
74 return json::readObject(asJson,
75 "tab_visible", tabVisible_,
76 "running", running_,
77 "target_file", targetFile_,
78 "output", output_,
79 "errors", errors_);
80 }
81
82
onStarted(const std::string & targetFile)83 void onStarted(const std::string& targetFile)
84 {
85 clear();
86 running_ = true;
87 tabVisible_ = true;
88 targetFile_ = targetFile;
89 }
90
addOutput(const std::string & output)91 void addOutput(const std::string& output)
92 {
93 output_ += output;
94 }
95
setErrors(const json::Array & errors)96 void setErrors(const json::Array& errors)
97 {
98 errors_ = errors;
99 }
100
onCompleted()101 void onCompleted()
102 {
103 running_ = false;
104 }
105
clear()106 void clear()
107 {
108 tabVisible_ = false;
109 running_ = false;
110 targetFile_.clear();
111 output_.clear();
112 errors_.clear();
113 }
114
asJson() const115 json::Object asJson() const
116 {
117 json::Object obj;
118 obj["tab_visible"] = tabVisible_;
119 obj["running"] = running_;
120 obj["target_file"] = targetFile_;
121 obj["output"] = output_;
122 obj["errors"] = errors_;
123 return obj;
124 }
125
126 private:
127 bool tabVisible_;
128 bool running_;
129 std::string targetFile_;
130 std::string output_;
131 json::Array errors_;
132 };
133
134 CompilePdfState s_compilePdfState;
135
onSuspend(Settings * pSettings)136 void onSuspend(Settings* pSettings)
137 {
138 pSettings->set("compile_pdf_state", s_compilePdfState.asJson().write());
139 }
140
141
onResume(const Settings & settings)142 void onResume(const Settings& settings)
143 {
144 std::string state = settings.get("compile_pdf_state");
145 if (!state.empty())
146 {
147 json::Value stateJson;
148 if (stateJson.parse(state))
149 {
150 LOG_WARNING_MESSAGE("invalid compile pdf state json");
151 return;
152 }
153
154 Error error = s_compilePdfState.readFromJson(stateJson.getObject());
155 if (error)
156 LOG_ERROR(error);
157 }
158 }
159
ancillaryFilePath(const FilePath & texFilePath,const std::string & ext)160 FilePath ancillaryFilePath(const FilePath& texFilePath, const std::string& ext)
161 {
162 return texFilePath.getParent().completeChildPath(texFilePath.getStem() + ext);
163 }
164
isSynctexAvailable(const FilePath & texFilePath)165 bool isSynctexAvailable(const FilePath& texFilePath)
166 {
167 return ancillaryFilePath(texFilePath, ".synctex.gz").exists() ||
168 ancillaryFilePath(texFilePath, ".synctex").exists();
169 }
170
enqueOutputEvent(const std::string & output)171 void enqueOutputEvent(const std::string& output)
172 {
173 s_compilePdfState.addOutput(output);
174
175 using namespace module_context;
176 CompileOutput compileOutput(kCompileOutputNormal, output);
177
178 ClientEvent event(client_events::kCompilePdfOutputEvent,
179 compileOutputAsJson(compileOutput));
180
181 module_context::enqueClientEvent(event);
182 }
183
enqueStartedEvent(const FilePath & texFilePath)184 void enqueStartedEvent(const FilePath& texFilePath)
185 {
186 std::string targetFile =
187 module_context::createAliasedPath(texFilePath);
188
189 s_compilePdfState.onStarted(targetFile);
190
191 json::Object dataJson;
192 dataJson["target_file"] = targetFile;
193 FilePath pdfPath = ancillaryFilePath(texFilePath, ".pdf");
194 dataJson["pdf_path"] = module_context::createAliasedPath(pdfPath);
195
196 ClientEvent event(client_events::kCompilePdfStartedEvent,
197 dataJson);
198
199 module_context::enqueClientEvent(event);
200 }
201
enqueCompletedEvent(bool succeeded,const json::Object & sourceLocation,const FilePath & texFilePath)202 void enqueCompletedEvent(bool succeeded,
203 const json::Object& sourceLocation,
204 const FilePath& texFilePath)
205 {
206 s_compilePdfState.onCompleted();
207
208 json::Object dataJson;
209 dataJson["succeeded"] = succeeded;
210 dataJson["target_file"] =
211 module_context::createAliasedPath(texFilePath);
212
213 FilePath pdfPath = ancillaryFilePath(texFilePath, ".pdf");
214 dataJson["pdf_path"] = module_context::createAliasedPath(pdfPath);
215 dataJson["view_pdf_url"] = tex::view_pdf::createViewPdfUrl(pdfPath);
216 bool synctexAvailable = isSynctexAvailable(texFilePath);
217 dataJson["synctex_available"] = synctexAvailable;
218 if (synctexAvailable)
219 {
220 json::Value pdfLocation;
221 Error error = modules::tex::synctex::forwardSearch(texFilePath,
222 sourceLocation,
223 &pdfLocation);
224 if (error)
225 LOG_ERROR(error);
226 dataJson["pdf_location"] = pdfLocation;
227 }
228
229 ClientEvent event(client_events::kCompilePdfCompletedEvent,
230 dataJson);
231 module_context::enqueClientEvent(event);
232 }
233
enqueCompletedWithFailureEvent(const FilePath & texFilePath,json::Object & sourceLocation)234 void enqueCompletedWithFailureEvent(const FilePath& texFilePath,
235 json::Object& sourceLocation)
236 {
237 enqueCompletedEvent(false, sourceLocation, texFilePath);
238 }
239
enqueCompletedWithSuccessEvent(const FilePath & texFilePath,const json::Object & sourceLocation)240 void enqueCompletedWithSuccessEvent(const FilePath& texFilePath,
241 const json::Object& sourceLocation)
242 {
243 enqueCompletedEvent(true, sourceLocation, texFilePath);
244 }
245
enqueErrorsEvent(const json::Array & logEntriesJson)246 void enqueErrorsEvent(const json::Array& logEntriesJson)
247 {
248 s_compilePdfState.setErrors(logEntriesJson);
249
250 ClientEvent event(client_events::kCompilePdfErrorsEvent, logEntriesJson);
251 module_context::enqueClientEvent(event);
252 }
253
254 // NOTE: sync changes with SessionModuleContext.cpp sourceMarkerJson
logEntryJson(const core::tex::LogEntry & logEntry)255 json::Object logEntryJson(const core::tex::LogEntry& logEntry)
256 {
257 json::Object obj;
258 obj["type"] = static_cast<int>(logEntry.type());
259 obj["path"] = module_context::createAliasedPath(logEntry.filePath());
260 obj["line"] = logEntry.line();
261 obj["column"] = 1;
262 obj["message"] = core::html_utils::HTML(logEntry.message()).text();
263 obj["log_path"] = module_context::createAliasedPath(logEntry.logFilePath());
264 obj["log_line"] = logEntry.logLine();
265 obj["show_error_list"] = true;
266 return obj;
267 }
268
showLogEntries(const core::tex::LogEntries & logEntries,const rnw_concordance::Concordances & rnwConcordances=rnw_concordance::Concordances ())269 void showLogEntries(const core::tex::LogEntries& logEntries,
270 const rnw_concordance::Concordances& rnwConcordances =
271 rnw_concordance::Concordances())
272 {
273 json::Array logEntriesJson;
274 for (const core::tex::LogEntry& logEntry : logEntries)
275 {
276 using namespace tex::rnw_concordance;
277 core::tex::LogEntry rnwEntry = rnwConcordances.fixup(logEntry);
278 logEntriesJson.push_back(logEntryJson(rnwEntry));
279 }
280
281 enqueErrorsEvent(logEntriesJson);
282 }
283
writeLogEntriesOutput(const core::tex::LogEntries & logEntries)284 void writeLogEntriesOutput(const core::tex::LogEntries& logEntries)
285 {
286 if (logEntries.empty())
287 return;
288
289 std::string output = "\n";
290 for (const core::tex::LogEntry& logEntry : logEntries)
291 {
292 switch(logEntry.type())
293 {
294 case core::tex::LogEntry::Error:
295 output += "Error: ";
296 break;
297 case core::tex::LogEntry::Warning:
298 output += "Warning: ";
299 break;
300 case core::tex::LogEntry::Box:
301 output += "Bad Box: ";
302 break;
303 }
304
305 output += logEntry.filePath().getFilename();
306 int line = logEntry.line();
307 if (line >= 0)
308 output += ":" + safe_convert::numberToString(line);
309
310 output += ": " + logEntry.message() + "\n";
311 }
312 output += "\n";
313
314 enqueOutputEvent(output);
315
316 }
317
318
includeLogEntry(const core::tex::LogEntry & logEntry)319 bool includeLogEntry(const core::tex::LogEntry& logEntry)
320 {
321 return true;
322 }
323
324 // filter out log entries which we view as superflous or distracting
filterLatexLog(const core::tex::LogEntries & logEntries,core::tex::LogEntries * pFilteredLogEntries)325 void filterLatexLog(const core::tex::LogEntries& logEntries,
326 core::tex::LogEntries* pFilteredLogEntries)
327 {
328 core::algorithm::copy_if(logEntries.begin(),
329 logEntries.end(),
330 std::back_inserter(*pFilteredLogEntries),
331 includeLogEntry);
332 }
333
isLogEntryFromTargetFile(const core::tex::LogEntry & logEntry,const FilePath & texPath)334 bool isLogEntryFromTargetFile(const core::tex::LogEntry& logEntry,
335 const FilePath& texPath)
336 {
337 return logEntry.filePath() == texPath;
338 }
339
getLogEntries(const FilePath & texPath,core::tex::LogEntries * pLogEntries)340 void getLogEntries(const FilePath& texPath,
341 core::tex::LogEntries* pLogEntries)
342 {
343 // latex log file
344 FilePath logPath = ancillaryFilePath(texPath, ".log");
345 if (logPath.exists())
346 {
347 core::tex::LogEntries logEntries;
348 Error error = core::tex::parseLatexLog(logPath, &logEntries);
349 if (error)
350 LOG_ERROR(error);
351
352 filterLatexLog(logEntries, pLogEntries);
353
354 // re-arrange so that issues in the target file always end up at the top
355 // of the error display
356 std::partition(pLogEntries->begin(),
357 pLogEntries->end(),
358 boost::bind(isLogEntryFromTargetFile, _1, texPath));
359 }
360
361 // bibtex log file
362 core::tex::LogEntries bibtexLogEntries;
363 logPath = ancillaryFilePath(texPath, ".blg");
364 if (logPath.exists())
365 {
366 Error error = core::tex::parseBibtexLog(logPath, &bibtexLogEntries);
367 if (error)
368 LOG_ERROR(error);
369 }
370
371 // concatenate them together
372 std::copy(bibtexLogEntries.begin(),
373 bibtexLogEntries.end(),
374 std::back_inserter(*pLogEntries));
375 }
376
removeExistingAncillary(const FilePath & texFilePath,const std::string & extension)377 void removeExistingAncillary(const FilePath& texFilePath,
378 const std::string& extension)
379 {
380 Error error = ancillaryFilePath(texFilePath, extension).removeIfExists();
381 if (error)
382 LOG_ERROR(error);
383 }
384
removeExistingLatexAncillaryFiles(const FilePath & texFilePath)385 void removeExistingLatexAncillaryFiles(const FilePath& texFilePath)
386 {
387 removeExistingAncillary(texFilePath, ".log");
388 removeExistingAncillary(texFilePath, ".blg");
389 removeExistingAncillary(texFilePath, ".synctex");
390 removeExistingAncillary(texFilePath, ".synctex.gz");
391 }
392
buildIssuesMessage(const core::tex::LogEntries & logEntries)393 std::string buildIssuesMessage(const core::tex::LogEntries& logEntries)
394 {
395 if (logEntries.empty())
396 return std::string();
397
398 // count error types
399 int errors = 0, warnings = 0, badBoxes = 0;
400 for (const core::tex::LogEntry& logEntry : logEntries)
401 {
402 if (logEntry.type() == core::tex::LogEntry::Error)
403 errors++;
404 else if (logEntry.type() == core::tex::LogEntry::Warning)
405 warnings++;
406 else if (logEntry.type() == core::tex::LogEntry::Box)
407 badBoxes++;
408 }
409
410 std::string issues;
411 boost::format fmt("%1% %2%");
412 if (errors > 0)
413 {
414 issues += boost::str(fmt % errors % "error");
415 if (errors > 1)
416 issues += "s";
417 }
418 if (warnings > 0)
419 {
420 if (!issues.empty())
421 issues += ", ";
422 issues += boost::str(fmt % warnings % "warning");
423 if (warnings > 1)
424 issues += "s";
425 }
426 if (badBoxes > 0)
427 {
428 if (!issues.empty())
429 issues += ", ";
430 issues += boost::str(fmt % badBoxes % "bad");
431 if (badBoxes > 1)
432 issues += "boxes";
433 else
434 issues += "box";
435 }
436
437 if (!issues.empty())
438 return "Issues: " + issues;
439 else
440 return std::string();
441 }
442
443 class AuxillaryFileCleanupContext : boost::noncopyable
444 {
445 public:
AuxillaryFileCleanupContext()446 AuxillaryFileCleanupContext()
447 : cleanLog_(true)
448 {
449 }
450
~AuxillaryFileCleanupContext()451 virtual ~AuxillaryFileCleanupContext()
452 {
453 try
454 {
455 cleanup();
456 }
457 catch(...)
458 {
459 }
460 }
461
init(const FilePath & targetFilePath)462 void init(const FilePath& targetFilePath)
463 {
464 basePath_ = targetFilePath.getParent().completeChildPath(
465 targetFilePath.getStem()).getAbsolutePath();
466 }
467
preserveLog()468 void preserveLog()
469 {
470 cleanLog_ = false;
471 }
472
preserveLogReferencedFiles(const core::tex::LogEntries & logEntries)473 void preserveLogReferencedFiles(
474 const core::tex::LogEntries& logEntries)
475 {
476 for (const core::tex::LogEntry& logEntry : logEntries)
477 {
478 logRefFiles_.insert(logEntry.filePath());
479 }
480 }
481
cleanup()482 void cleanup()
483 {
484 if (!basePath_.empty())
485 {
486 // remove known auxillary files
487 remove(".out");
488 remove(".aux");
489
490 // only clean bbl if .bib exists
491 if (exists(".bib"))
492 remove(".bbl");
493
494 // clean anciallary logs if requested (never clean latex log)
495 if (cleanLog_)
496 {
497 remove(".blg");
498 }
499
500 // reset base path so we only do this once
501 basePath_.clear();
502 }
503 }
504
505 private:
exists(const std::string & extension)506 bool exists(const std::string& extension)
507 {
508 return FilePath(basePath_ + extension).exists();
509 }
510
511 // remove the specified file (but don't if it's referenced
512 // from the log)
remove(const std::string & extension)513 void remove(const std::string& extension)
514 {
515 FilePath filePath(basePath_ + extension);
516 if (logRefFiles_.find(filePath) == logRefFiles_.end())
517 {
518 Error error = filePath.removeIfExists();
519 if (error)
520 LOG_ERROR(error);
521 }
522 }
523
524 private:
525 std::string basePath_;
526 bool cleanLog_;
527 std::set<FilePath> logRefFiles_;
528 };
529
530 // implement pdf compilation within a class so we can maintain state
531 // accross the various async callbacks the compile is composed of
532 class AsyncPdfCompiler : boost::noncopyable,
533 public boost::enable_shared_from_this<AsyncPdfCompiler>
534 {
535 public:
start(const FilePath & targetFilePath,const std::string & encoding,const json::Object & sourceLocation,const boost::function<void ()> & onCompleted)536 static void start(const FilePath& targetFilePath,
537 const std::string& encoding,
538 const json::Object& sourceLocation,
539 const boost::function<void()>& onCompleted)
540 {
541 boost::shared_ptr<AsyncPdfCompiler> pCompiler(
542 new AsyncPdfCompiler(targetFilePath,
543 encoding,
544 sourceLocation,
545 onCompleted));
546
547 pCompiler->start();
548 }
549
~AsyncPdfCompiler()550 virtual ~AsyncPdfCompiler() {}
551
552 private:
AsyncPdfCompiler(const FilePath & targetFilePath,const std::string & encoding,const json::Object & sourceLocation,const boost::function<void ()> & onCompleted)553 AsyncPdfCompiler(const FilePath& targetFilePath,
554 const std::string& encoding,
555 const json::Object& sourceLocation,
556 const boost::function<void()>& onCompleted)
557 : targetFilePath_(targetFilePath),
558 encoding_(encoding),
559 sourceLocation_(sourceLocation),
560 onCompleted_(onCompleted)
561 {
562 if (targetFilePath_.exists())
563 {
564 Error error = core::system::realPath(targetFilePath_, &targetFilePath_);
565 if (error)
566 LOG_ERROR(error);
567 }
568 }
569
start()570 void start()
571 {
572 // enque started event
573 enqueStartedEvent(targetFilePath_);
574
575 // terminate if the file doesn't exist
576 if (!targetFilePath_.exists())
577 {
578 terminateWithError("Target document not found: '" +
579 targetFilePath_.getAbsolutePath() + "'");
580 return;
581 }
582
583 // ensure no spaces in path
584 std::string filename = targetFilePath_.getFilename();
585 if (filename.find(' ') != std::string::npos)
586 {
587 terminateWithError("Invalid filename: '" + filename +
588 "' (TeX does not understand paths with spaces)");
589 return;
590 }
591
592 // parse magic comments
593 Error error = core::tex::parseMagicComments(targetFilePath_,
594 &magicComments_);
595 if (error)
596 LOG_ERROR(error);
597
598 // add tinytex to the PATH if appropriate
599 module_context::addTinytexToPathIfNecessary();
600
601 // determine tex program path
602 std::string userErrMsg;
603 if (!pdflatex::latexProgramForFile(magicComments_,
604 &texProgramPath_,
605 &userErrMsg))
606 {
607 terminateWithError(userErrMsg);
608 return;
609 }
610
611 // see if we need to weave
612 std::string ext = targetFilePath_.getExtensionLowerCase();
613 bool isRnw = ext == ".rnw" || ext == ".snw" || ext == ".nw" || ext == ".rtex";
614 if (isRnw)
615 {
616 // remove existing ancillary files + concordance
617 removeExistingLatexAncillaryFiles(targetFilePath_);
618 removeExistingAncillary(targetFilePath_, "-concordance.tex");
619
620 // attempt to weave the rnw
621 rnw_weave::runWeave(targetFilePath_,
622 encoding_,
623 magicComments_,
624 enqueOutputEvent,
625 boost::bind(
626 &AsyncPdfCompiler::onWeaveCompleted,
627 AsyncPdfCompiler::shared_from_this(), _1));
628 }
629 else if (prefs::userPrefs().useTinytex())
630 {
631 runTinytex(targetFilePath_);
632 }
633 else
634 {
635 runLatexCompiler(false);
636 }
637
638 }
639
640 private:
641
onWeaveCompleted(const rnw_weave::Result & result)642 void onWeaveCompleted(const rnw_weave::Result& result)
643 {
644 if (result.succeeded)
645 {
646 if (prefs::userPrefs().useTinytex())
647 {
648 // compute tex file path
649 std::string texFileName = targetFilePath_.getStem() + ".tex";
650 FilePath texFilePath = targetFilePath_.getParent().completePath(texFileName);
651 runTinytex(texFilePath);
652 }
653 else
654 {
655 runLatexCompiler(true, result.concordances);
656 }
657 }
658 else if (!result.errorLogEntries.empty())
659 terminateWithErrorLogEntries(result.errorLogEntries);
660 else
661 terminateWithError(result.errorMessage);
662 }
663
onTinytexOutput(const std::string & output)664 void onTinytexOutput(const std::string& output)
665 {
666 enqueOutputEvent(output);
667 }
668
onTinytexCompileCompleted(int status,const std::string & output)669 void onTinytexCompileCompleted(int status, const std::string& output)
670 {
671 onLatexCompileCompleted(status, targetFilePath_);
672 }
673
runTinytex(const FilePath & latexFilePath)674 void runTinytex(const FilePath& latexFilePath)
675 {
676 Error error;
677
678 // build arguments
679 using Argument = std::pair<std::string, std::string>;
680 std::vector<Argument> latexmkArgs;
681
682 std::string file = latexFilePath.getAbsolutePath();
683 latexmkArgs.push_back({std::string(), shell_utils::escape(file)});
684
685 std::string engine = string_utils::toLower(prefs::userPrefs().defaultLatexProgram());
686 latexmkArgs.push_back({"engine", shell_utils::escape(engine)});
687
688 bool clean = prefs::userPrefs().cleanTexi2dviOutput();
689 latexmkArgs.push_back({"clean", clean ? "TRUE" : "FALSE"});
690
691 if (prefs::userPrefs().latexShellEscape())
692 {
693 latexmkArgs.push_back({"engine_args", shell_utils::escape("-shell-escape")});
694 }
695
696 auto collapse = [](const Argument& argument)
697 {
698 return argument.first.empty()
699 ? argument.second
700 : argument.first + " = " + argument.second;
701 };
702
703 std::string arguments = core::algorithm::join(
704 latexmkArgs.begin(),
705 latexmkArgs.end(),
706 ", ",
707 collapse);
708
709 std::string code =
710 "cat('Compiling document with tinytex ... ');"
711 "invisible(tinytex::latexmk(" + arguments + "))";
712
713 codePath_ = module_context::tempFile("tinytex-runner-", ".R");
714 error = writeStringToFile(codePath_, code);
715 if (error)
716 {
717 terminateWithError(error.getSummary());
718 return;
719 }
720
721 FilePath rScriptPath;
722 error = module_context::rScriptPath(&rScriptPath);
723 if (error)
724 {
725 terminateWithError(error.getSummary());
726 return;
727 }
728
729 std::vector<std::string> args;
730 args.push_back("-s");
731 args.push_back("-f");
732 args.push_back(string_utils::utf8ToSystem(codePath_.getAbsolutePath()));
733
734 error = compile_pdf_supervisor::runProgram(
735 rScriptPath,
736 args,
737 tex::utils::rTexInputsEnvVars(),
738 latexFilePath.getParent(),
739 boost::bind(
740 &AsyncPdfCompiler::onTinytexOutput,
741 AsyncPdfCompiler::shared_from_this(),
742 _1),
743 boost::bind(
744 &AsyncPdfCompiler::onTinytexCompileCompleted,
745 AsyncPdfCompiler::shared_from_this(),
746 _1,
747 _2));
748
749 if (error)
750 terminateWithError("Unable to compile pdf: " + error.getSummary());
751
752 }
753
runLatexCompiler(bool targetWeaved,const rnw_concordance::Concordances & concordances=rnw_concordance::Concordances ())754 void runLatexCompiler(bool targetWeaved,
755 const rnw_concordance::Concordances& concordances =
756 rnw_concordance::Concordances())
757 {
758 // configure pdflatex options
759 pdflatex::PdfLatexOptions options;
760 options.fileLineError = false;
761 options.syncTex = !isTargetRnw() || !concordances.empty();
762 options.shellEscape = prefs::userPrefs().latexShellEscape();
763
764 // get back-end version info
765 core::system::ProcessResult result;
766 Error error = core::system::runProgram(
767 string_utils::utf8ToSystem(texProgramPath_.getAbsolutePath()),
768 core::shell_utils::ShellArgs() << "--version",
769 "",
770 core::system::ProcessOptions(),
771 &result);
772 if (error)
773 LOG_ERROR(error);
774 else if (result.exitStatus != EXIT_SUCCESS)
775 LOG_ERROR_MESSAGE("Error probing for latex version: "+ result.stdErr);
776 else
777 options.versionInfo = result.stdOut;
778
779 // compute tex file path
780 FilePath texFilePath;
781 if (targetWeaved)
782 {
783 texFilePath = targetFilePath_.getParent().completePath(
784 targetFilePath_.getStem() + ".tex");
785 }
786 else
787 {
788 texFilePath = targetFilePath_;
789 }
790
791 // remove log files if they exist (avoids confusion created by parsing
792 // old log files for errors)
793 removeExistingLatexAncillaryFiles(texFilePath);
794
795 // setup cleanup context if clean was specified
796 if (prefs::userPrefs().cleanTexi2dviOutput())
797 auxillaryFileCleanupContext_.init(texFilePath);
798
799 // run latex compile
800
801 // this is our "simulated" texi2dvi -- this was originally
802 // coded as a sequence of sync calls to pdflatex, bibtex, and
803 // makeindex. re-coding it as async is going to be a bit
804 // involved so considering that this is not the default
805 // codepath we'll leave it sync for now (and then just call
806 // the (typically) async callback function onLatexCompileCompleted
807 // directly after the function returns
808
809 enqueOutputEvent("Running " + texProgramPath_.getFilename() +
810 " on " + texFilePath.getFilename() + "...");
811
812 error = tex::pdflatex::texToPdf(texProgramPath_,
813 texFilePath,
814 options,
815 &result);
816
817 if (error)
818 {
819 terminateWithError("Unable to compile pdf: " + error.getSummary());
820 }
821 else
822 {
823 onLatexCompileCompleted(result.exitStatus,
824 texFilePath,
825 concordances);
826 }
827
828 }
829
onLatexCompileCompleted(int exitStatus,const FilePath & texFilePath,const rnw_concordance::Concordances & concords=rnw_concordance::Concordances ())830 void onLatexCompileCompleted(int exitStatus,
831 const FilePath& texFilePath,
832 const rnw_concordance::Concordances& concords = rnw_concordance::Concordances())
833 {
834 // remove code file if we had one
835 codePath_.removeIfExists();
836
837 // collect errors from the log
838 core::tex::LogEntries logEntries;
839 getLogEntries(texFilePath, &logEntries);
840
841 // determine whether they will be shown in the list
842 // list or within the console
843 bool showIssuesList = !isTargetRnw() || !concords.empty();
844
845 // notify the cleanp context of log entries (so it can
846 // preserve any referenced files)
847 auxillaryFileCleanupContext_.preserveLogReferencedFiles(
848 logEntries);
849
850 // show log entries and build issues message
851 std::string issuesMsg;
852 if (showIssuesList && !logEntries.empty())
853 {
854 showLogEntries(logEntries, concords);
855 issuesMsg = buildIssuesMessage(logEntries);
856 }
857
858 if (exitStatus == EXIT_SUCCESS)
859 {
860 FilePath pdfPath = ancillaryFilePath(texFilePath, ".pdf");
861 std::string pdfFile = module_context::createAliasedPath(
862 pdfPath);
863 std::string completed = "completed\n\nCreated PDF: " + pdfFile + "\n";
864 if (!issuesMsg.empty())
865 completed += "\n" + issuesMsg;
866 enqueOutputEvent(completed);
867
868 // show issues in console if necessary
869 if (!showIssuesList)
870 writeLogEntriesOutput(logEntries);
871
872 if (onCompleted_)
873 onCompleted_();
874
875 enqueCompletedWithSuccessEvent(targetFilePath_, sourceLocation_);
876 }
877 else
878 {
879 std::string failedMsg = "failed\n";
880 if (!issuesMsg.empty())
881 failedMsg += "\n" + issuesMsg;
882 enqueOutputEvent(failedMsg);
883
884 // don't remove the log
885 auxillaryFileCleanupContext_.preserveLog();
886
887 // if there were no error found in the log file then just
888 // print the error and exit code
889 if (logEntries.empty())
890 {
891 boost::format fmt("Error running %1% (exit code %2%)");
892 std::string msg(boost::str(fmt % texProgramPath_.getAbsolutePath()
893 % exitStatus));
894 enqueOutputEvent(msg + "\n");
895 }
896
897 // show issues in console if necessary
898 if (!showIssuesList)
899 writeLogEntriesOutput(logEntries);
900
901 enqueCompletedWithFailureEvent(targetFilePath_, sourceLocation_);
902 }
903 }
904
terminateWithError(const std::string & message)905 void terminateWithError(const std::string& message)
906 {
907 enqueOutputEvent(message + "\n");
908 enqueCompletedWithFailureEvent(targetFilePath_, sourceLocation_);
909 }
910
terminateWithErrorLogEntries(const core::tex::LogEntries & logEntries)911 void terminateWithErrorLogEntries(const core::tex::LogEntries& logEntries)
912 {
913 showLogEntries(logEntries);
914 enqueCompletedWithFailureEvent(targetFilePath_, sourceLocation_);
915 }
916
isTargetRnw() const917 bool isTargetRnw() const
918 {
919 return targetFilePath_.getExtensionLowerCase() == ".rnw" ||
920 targetFilePath_.getExtensionLowerCase() == ".rtex";
921 }
922
923 private:
924 FilePath targetFilePath_;
925 FilePath codePath_;
926 std::string encoding_;
927 json::Object sourceLocation_;
928 const boost::function<void()> onCompleted_;
929 core::tex::TexMagicComments magicComments_;
930 FilePath texProgramPath_;
931 AuxillaryFileCleanupContext auxillaryFileCleanupContext_;
932 };
933
934
935 } // anonymous namespace
936
937
startCompile(const core::FilePath & targetFilePath,const std::string & encoding,const json::Object & sourceLocation,const boost::function<void ()> & onCompleted)938 bool startCompile(const core::FilePath& targetFilePath,
939 const std::string& encoding,
940 const json::Object& sourceLocation,
941 const boost::function<void()>& onCompleted)
942 {
943 if (!compile_pdf_supervisor::hasRunningChildren())
944 {
945 AsyncPdfCompiler::start(targetFilePath,
946 encoding,
947 sourceLocation,
948 onCompleted);
949 return true;
950 }
951 else
952 {
953 return false;
954 }
955 }
956
compileIsRunning()957 bool compileIsRunning()
958 {
959 return compile_pdf_supervisor::hasRunningChildren();
960 }
961
terminateCompile()962 bool terminateCompile()
963 {
964 Error error = compile_pdf_supervisor::terminateAll(
965 boost::posix_time::seconds(1));
966 if (error)
967 {
968 LOG_ERROR(error);
969 return false;
970 }
971 else
972 {
973 enqueOutputEvent("\n[Compile PDF Stopped]\n");
974 return true;
975 }
976 }
977
notifyTabClosed()978 void notifyTabClosed()
979 {
980 s_compilePdfState.clear();
981 }
982
currentStateAsJson()983 json::Object currentStateAsJson()
984 {
985 return s_compilePdfState.asJson();
986 }
987
initialize()988 Error initialize()
989 {
990 // register suspend handler
991 using namespace module_context;
992 addSuspendHandler(SuspendHandler(boost::bind(onSuspend, _2), onResume));
993
994 return Success();
995 }
996
997 } // namespace compile_pdf
998 } // namespace tex
999 } // namespace modules
1000 } // namespace session
1001 } // namespace rstudio
1002
1003