1 /*
2 * RProjectFile.cpp
3 *
4 * Copyright (C) 2021 by RStudio, PBC
5 *
6 * Unless you have received this program directly from RStudio pursuant
7 * to the terms of a commercial license agreement with RStudio, then
8 * this program is licensed to you under the terms of version 3 of the
9 * GNU Affero General Public License. This program is distributed WITHOUT
10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13 *
14 */
15
16 #include <core/r_util/RProjectFile.hpp>
17
18 #include <map>
19 #include <iomanip>
20 #include <ostream>
21
22 #include <boost/format.hpp>
23 #include <boost/regex.hpp>
24
25 #include <shared_core/Error.hpp>
26 #include <shared_core/FilePath.hpp>
27 #include <core/FileSerializer.hpp>
28 #include <shared_core/SafeConvert.hpp>
29 #include <core/RegexUtils.hpp>
30 #include <core/StringUtils.hpp>
31 #include <core/YamlUtil.hpp>
32 #include <core/text/DcfParser.hpp>
33 #include <core/text/CsvParser.hpp>
34 #include <core/system/Environment.hpp>
35
36 #include <core/r_util/RPackageInfo.hpp>
37 #include <core/r_util/RVersionInfo.hpp>
38
39 namespace rstudio {
40 namespace core {
41 namespace r_util {
42
43 const int kLineEndingsUseDefault = -1;
44
45 const char * const kLineEndingPassthough = "None";
46 const char * const kLineEndingNative = "Native";
47 const char * const kLineEndingWindows = "Windows";
48 const char * const kLineEndingPosix = "Posix";
49
50 const char * const kBuildTypeNone = "None";
51 const char * const kBuildTypePackage = "Package";
52 const char * const kBuildTypeMakefile = "Makefile";
53 const char * const kBuildTypeWebsite = "Website";
54 const char * const kBuildTypeCustom = "Custom";
55 const char * const kBuildTypeQuarto = "Quarto";
56
57 const char * const kMarkdownWrapUseDefault = "Default";
58 const char * const kMarkdownWrapNone = "None";
59 const char * const kMarkdownWrapColumn = "Column";
60 const char * const kMarkdownWrapSentence = "Sentence";
61
62 const int kMarkdownWrapAtColumnDefault = 72;
63
64 const char * const kMarkdownReferencesUseDefault = "Default";
65 const char * const kMarkdownReferencesBlock = "Block";
66 const char * const kMarkdownReferencesSection = "Section";
67 const char * const kMarkdownReferencesDocument = "Document";
68
69 const char * const kZoteroLibrariesAll = "All";
70
71 namespace {
72
73 const char * const kPackageInstallArgsDefault = "--no-multiarch "
74 "--with-keep.source";
75 const char * const kPackageInstallArgsPreviousDefault = "--no-multiarch";
76
requiredFieldError(const std::string & field,std::string * pUserErrMsg)77 Error requiredFieldError(const std::string& field,
78 std::string* pUserErrMsg)
79 {
80 *pUserErrMsg = field + " not correctly specified in project config file";
81 Error error = systemError(boost::system::errc::protocol_error, ERROR_LOCATION);
82 error.addProperty("user-msg", *pUserErrMsg);
83 return error;
84 }
85
yesNoAskValueToString(int value)86 std::string yesNoAskValueToString(int value)
87 {
88 std::ostringstream ostr;
89 ostr << (YesNoAskValue) value;
90 return ostr.str();
91 }
92
interpretYesNoAskValue(const std::string & value,bool acceptAsk,int * pValue)93 bool interpretYesNoAskValue(const std::string& value,
94 bool acceptAsk,
95 int* pValue)
96 {
97 std::string valueLower = string_utils::toLower(value);
98 boost::algorithm::trim(valueLower);
99 if (valueLower == "yes")
100 {
101 *pValue = YesValue;
102 return true;
103 }
104 else if (valueLower == "no")
105 {
106 *pValue = NoValue;
107 return true;
108 }
109 else if (valueLower == "default")
110 {
111 *pValue = DefaultValue;
112 return true;
113 }
114 else if (acceptAsk && (valueLower == "ask"))
115 {
116 *pValue = AskValue;
117 return true;
118 }
119 else
120 {
121 return false;
122 }
123 }
124
boolValueToString(bool value)125 std::string boolValueToString(bool value)
126 {
127 if (value)
128 return "Yes";
129 else
130 return "No";
131 }
132
interpretBoolValue(const std::string & value,bool * pValue)133 bool interpretBoolValue(const std::string& value, bool* pValue)
134 {
135 std::string valueLower = string_utils::toLower(value);
136 boost::algorithm::trim(valueLower);
137 if (valueLower == "yes")
138 {
139 *pValue = true;
140 return true;
141 }
142 else if (valueLower == "no")
143 {
144 *pValue = false;
145 return true;
146 }
147 else
148 {
149 return false;
150 }
151 }
152
interpretBuildTypeValue(const std::string & value,std::string * pValue)153 bool interpretBuildTypeValue(const std::string& value, std::string* pValue)
154 {
155 if (value == "" ||
156 value == kBuildTypeNone ||
157 value == kBuildTypePackage ||
158 value == kBuildTypeMakefile ||
159 value == kBuildTypeWebsite ||
160 value == kBuildTypeCustom)
161 {
162 *pValue = value;
163 return true;
164 }
165 else
166 {
167 return false;
168 }
169 }
170
interpretLineEndingsValue(std::string value,int * pValue)171 bool interpretLineEndingsValue(std::string value, int* pValue)
172 {
173 value = boost::algorithm::trim_copy(value);
174 if (value == "")
175 {
176 *pValue = kLineEndingsUseDefault;
177 return true;
178 }
179 else if (value == kLineEndingPassthough)
180 {
181 *pValue = string_utils::LineEndingPassthrough;
182 return true;
183 }
184 else if (value == kLineEndingNative)
185 {
186 *pValue = string_utils::LineEndingNative;
187 return true;
188 }
189 else if (value == kLineEndingWindows)
190 {
191 *pValue = string_utils::LineEndingWindows;
192 return true;
193 }
194 else if (value == kLineEndingPosix)
195 {
196 *pValue = string_utils::LineEndingPosix;
197 return true;
198 }
199 else
200 {
201 return false;
202 }
203 }
204
interpretMarkdownWrapValue(const std::string & value,std::string * pValue)205 bool interpretMarkdownWrapValue(const std::string& value, std::string* pValue)
206 {
207 if (value == "" || value == kMarkdownWrapUseDefault)
208 {
209 *pValue = kMarkdownWrapUseDefault;
210 return true;
211 }
212 else if (value == kMarkdownWrapNone ||
213 value == kMarkdownWrapColumn ||
214 value == kMarkdownWrapSentence)
215 {
216 *pValue = value;
217 return true;
218 }
219 else
220 {
221 return false;
222 }
223 }
224
interpretMarkdownReferencesValue(const std::string & value,std::string * pValue)225 bool interpretMarkdownReferencesValue(const std::string& value, std::string *pValue)
226 {
227 if (value == "" || value == kMarkdownReferencesUseDefault)
228 {
229 *pValue = kMarkdownReferencesUseDefault;
230 return true;
231 }
232 else if (value == kMarkdownReferencesBlock ||
233 value == kMarkdownReferencesSection ||
234 value == kMarkdownReferencesDocument)
235 {
236 *pValue = value;
237 return true;
238 }
239 else
240 {
241 return false;
242 }
243
244 }
245
interpretZoteroLibraries(const std::string & value,boost::optional<std::vector<std::string>> * pValue)246 void interpretZoteroLibraries(const std::string& value, boost::optional<std::vector<std::string>>* pValue)
247 {
248 if (value == "")
249 {
250 pValue->reset(std::vector<std::string>());
251 }
252 else if (value == kZoteroLibrariesAll) // migration
253 {
254 pValue->reset(std::vector<std::string>( { "My Library" }));
255 }
256 else
257 {
258 std::vector<std::string> parsedLibraries;
259 text::parseCsvLine(value.begin(), value.end(), true, &parsedLibraries);
260 std::vector<std::string> libraries;
261 std::transform(parsedLibraries.begin(), parsedLibraries.end(), std::back_inserter(libraries), [](const std::string& str) {
262 return boost::algorithm::trim_copy(str);
263 });
264 pValue->reset(libraries);
265 }
266 }
267
268
interpretIntValue(const std::string & value,int * pValue)269 bool interpretIntValue(const std::string& value, int* pValue)
270 {
271 try
272 {
273 *pValue = boost::lexical_cast<int>(value);
274 return true;
275 }
276 catch(const boost::bad_lexical_cast&)
277 {
278 return false;
279 }
280 }
281
setBuildPackageDefaults(const std::string & packagePath,const RProjectBuildDefaults & buildDefaults,RProjectConfig * pConfig)282 void setBuildPackageDefaults(const std::string& packagePath,
283 const RProjectBuildDefaults& buildDefaults,
284 RProjectConfig* pConfig)
285 {
286 pConfig->buildType = kBuildTypePackage;
287 pConfig->packageUseDevtools = buildDefaults.useDevtools;
288 pConfig->packagePath = packagePath;
289 pConfig->packageInstallArgs = kPackageInstallArgsDefault;
290 }
291
detectBuildType(const FilePath & projectFilePath,const RProjectBuildDefaults & buildDefaults,RProjectConfig * pConfig)292 std::string detectBuildType(const FilePath& projectFilePath,
293 const RProjectBuildDefaults& buildDefaults,
294 RProjectConfig* pConfig)
295 {
296 FilePath projectDir = projectFilePath.getParent();
297 if (r_util::isPackageDirectory(projectDir))
298 {
299 setBuildPackageDefaults("", buildDefaults ,pConfig);
300 }
301 else if (projectDir.completeChildPath("pkg/DESCRIPTION").exists())
302 {
303 setBuildPackageDefaults("pkg", buildDefaults, pConfig);
304 }
305 else if (projectDir.completeChildPath("Makefile").exists())
306 {
307 pConfig->buildType = kBuildTypeMakefile;
308 pConfig->makefilePath = "";
309 }
310 else if (isWebsiteDirectory(projectDir))
311 {
312 pConfig->buildType = kBuildTypeWebsite;
313 pConfig->websitePath = "";
314 }
315 else
316 {
317 pConfig->buildType = kBuildTypeNone;
318 }
319
320 return pConfig->buildType;
321 }
322
detectBuildType(const FilePath & projectFilePath,const RProjectBuildDefaults & buildDefaults)323 std::string detectBuildType(const FilePath& projectFilePath,
324 const RProjectBuildDefaults& buildDefaults)
325 {
326 RProjectConfig config;
327 return detectBuildType(projectFilePath, buildDefaults, &config);
328 }
329
rVersionAsString(const RVersionInfo & rVersion)330 std::string rVersionAsString(const RVersionInfo& rVersion)
331 {
332 std::string ver = rVersion.number;
333 if (!rVersion.arch.empty())
334 ver += ("/" + rVersion.arch);
335 return ver;
336 }
337
rVersionFromString(const std::string & str)338 RVersionInfo rVersionFromString(const std::string& str)
339 {
340 std::size_t pos = str.find('/');
341 if (pos == std::string::npos)
342 return RVersionInfo(str);
343 else
344 return RVersionInfo(str.substr(0, pos), str.substr(pos+1));
345 }
346
interpretRVersionValue(const std::string & value,RVersionInfo * pRVersion)347 bool interpretRVersionValue(const std::string& value,
348 RVersionInfo* pRVersion)
349 {
350 RVersionInfo version = rVersionFromString(value);
351
352 if (version.number != kRVersionDefault &&
353 !regex_utils::match(version.number, boost::regex("[\\d\\.]+")))
354 {
355 return false;
356 }
357 else if (version.arch != "" &&
358 version.arch != kRVersionArch32 &&
359 version.arch != kRVersionArch64)
360 {
361 return false;
362 }
363 else
364 {
365 *pRVersion = version;
366 return true;
367 }
368 }
369
370 } // anonymous namespace
371
operator <<(std::ostream & stream,const YesNoAskValue & val)372 std::ostream& operator << (std::ostream& stream, const YesNoAskValue& val)
373 {
374 switch(val)
375 {
376 case YesValue:
377 stream << "Yes";
378 break;
379 case NoValue:
380 stream << "No";
381 break;
382 case AskValue:
383 stream << "Ask";
384 break;
385 case DefaultValue:
386 default:
387 stream << "Default";
388 break;
389 }
390
391 return stream;
392 }
393
findProjectFile(FilePath filePath,FilePath anchorPath,FilePath * pProjPath)394 Error findProjectFile(FilePath filePath,
395 FilePath anchorPath,
396 FilePath* pProjPath)
397 {
398 // check to see if we already have a .Rproj file
399 if (filePath.getExtensionLowerCase() == ".rproj")
400 {
401 *pProjPath = filePath;
402 return Success();
403 }
404
405 if (!filePath.isDirectory())
406 filePath = filePath.getParent();
407
408 // list all paths up to root for our anchor -- we want to stop looking
409 // if we hit the anchor path, or any parent directory of that path
410 std::vector<FilePath> anchorPaths;
411 for (; anchorPath.exists(); anchorPath = anchorPath.getParent())
412 anchorPaths.push_back(anchorPath);
413
414 // no .Rproj file found; scan parent directories
415 for (; filePath.exists(); filePath = filePath.getParent())
416 {
417 // bail if we've hit our anchor
418 for (const FilePath& anchorPath : anchorPaths)
419 {
420 if (filePath == anchorPath)
421 return fileNotFoundError(ERROR_LOCATION);
422 }
423
424 // skip directory if there's no .Rproj.user directory (avoid potentially
425 // expensive query of all files in directory)
426 if (!filePath.completePath(".Rproj.user").exists())
427 continue;
428
429 // scan this directory for .Rproj files
430 FilePath projPath = projectFromDirectory(filePath);
431 if (!projPath.isEmpty())
432 {
433 *pProjPath = projPath;
434 return Success();
435 }
436 }
437
438 return fileNotFoundError(ERROR_LOCATION);
439 }
440
findProjectConfig(FilePath filePath,const FilePath & anchorPath,RProjectConfig * pConfig)441 Error findProjectConfig(FilePath filePath,
442 const FilePath& anchorPath,
443 RProjectConfig* pConfig)
444 {
445 Error error;
446
447 FilePath projPath;
448 error = findProjectFile(filePath, anchorPath, &projPath);
449 if (error)
450 return error;
451
452 std::string errorMessage;
453 error = readProjectFile(projPath, pConfig, &errorMessage);
454 if (error)
455 return error;
456
457 return Success();
458 }
459
readProjectFile(const FilePath & projectFilePath,RProjectConfig * pConfig,std::string * pUserErrMsg)460 Error readProjectFile(const FilePath& projectFilePath,
461 RProjectConfig* pConfig,
462 std::string* pUserErrMsg)
463 {
464 bool providedDefaults;
465 return readProjectFile(projectFilePath,
466 RProjectConfig(),
467 RProjectBuildDefaults(),
468 pConfig,
469 &providedDefaults,
470 pUserErrMsg);
471 }
472
473 // TODO: add visual markdown options, e.g. see:
474 // https://github.com/rstudio/rstudio/commit/35126fd3b7f1b2f78dcc8f9df6c77f1a1bd324dc#diff-87efb91b81672d3e9b3a9c9c6e46241c
475
readProjectFile(const FilePath & projectFilePath,const RProjectConfig & defaultConfig,const RProjectBuildDefaults & buildDefaults,RProjectConfig * pConfig,bool * pProvidedDefaults,std::string * pUserErrMsg)476 Error readProjectFile(const FilePath& projectFilePath,
477 const RProjectConfig& defaultConfig,
478 const RProjectBuildDefaults& buildDefaults,
479 RProjectConfig* pConfig,
480 bool* pProvidedDefaults,
481 std::string* pUserErrMsg)
482 {
483 // default to not providing defaults
484 *pProvidedDefaults = false;
485
486 // first read the project DCF file
487 typedef std::map<std::string,std::string> Fields;
488 Fields dcfFields;
489 Error error = text::parseDcfFile(projectFilePath,
490 true,
491 &dcfFields,
492 pUserErrMsg);
493 if (error)
494 return error;
495
496 // extract version
497 Fields::const_iterator it = dcfFields.find("Version");
498
499 // no version field
500 if (it == dcfFields.end())
501 {
502 *pUserErrMsg = "The project file did not include a Version attribute "
503 "(it may have been created by a more recent version "
504 "of RStudio)";
505 return systemError(boost::system::errc::protocol_error,
506 ERROR_LOCATION);
507 }
508
509 // invalid version field
510 pConfig->version = safe_convert::stringTo<double>(it->second, 0.0);
511 if (pConfig->version == 0.0)
512 {
513 return requiredFieldError("Version", pUserErrMsg);
514 }
515
516 // version later than 1.0
517 if (pConfig->version != 1.0)
518 {
519 *pUserErrMsg = "The project file was created by a more recent "
520 "version of RStudio";
521 return systemError(boost::system::errc::protocol_error,
522 ERROR_LOCATION);
523 }
524
525 // extract R version
526 it = dcfFields.find("RVersion");
527 if (it != dcfFields.end())
528 {
529 if (!interpretRVersionValue(it->second, &(pConfig->rVersion)))
530 return requiredFieldError("RVersion", pUserErrMsg);
531 }
532 else
533 {
534 pConfig->rVersion = defaultConfig.rVersion;
535 }
536
537 // extract restore workspace
538 it = dcfFields.find("RestoreWorkspace");
539 if (it != dcfFields.end())
540 {
541 if (!interpretYesNoAskValue(it->second, false, &(pConfig->restoreWorkspace)))
542 return requiredFieldError("RestoreWorkspace", pUserErrMsg);
543 }
544 else
545 {
546 pConfig->restoreWorkspace = defaultConfig.restoreWorkspace;
547 *pProvidedDefaults = true;
548 }
549
550 // extract save workspace
551 it = dcfFields.find("SaveWorkspace");
552 if (it != dcfFields.end())
553 {
554 if (!interpretYesNoAskValue(it->second, true, &(pConfig->saveWorkspace)))
555 return requiredFieldError("SaveWorkspace", pUserErrMsg);
556 }
557 else
558 {
559 pConfig->saveWorkspace = defaultConfig.saveWorkspace;
560 *pProvidedDefaults = true;
561 }
562
563 // extract always save history
564 it = dcfFields.find("AlwaysSaveHistory");
565 if (it != dcfFields.end())
566 {
567 if (!interpretYesNoAskValue(it->second, false, &(pConfig->alwaysSaveHistory)))
568 return requiredFieldError("AlwaysSaveHistory", pUserErrMsg);
569 }
570 else
571 {
572 pConfig->alwaysSaveHistory = defaultConfig.alwaysSaveHistory;
573 *pProvidedDefaults = true;
574 }
575
576 // extract enable code indexing
577 it = dcfFields.find("EnableCodeIndexing");
578 if (it != dcfFields.end())
579 {
580 if (!interpretBoolValue(it->second, &(pConfig->enableCodeIndexing)))
581 return requiredFieldError("EnableCodeIndexing", pUserErrMsg);
582 }
583 else
584 {
585 pConfig->enableCodeIndexing = defaultConfig.enableCodeIndexing;
586 *pProvidedDefaults = true;
587 }
588
589 // extract spaces for tab
590 it = dcfFields.find("UseSpacesForTab");
591 if (it != dcfFields.end())
592 {
593 if (!interpretBoolValue(it->second, &(pConfig->useSpacesForTab)))
594 return requiredFieldError("UseSpacesForTab", pUserErrMsg);
595 }
596 else
597 {
598 pConfig->useSpacesForTab = defaultConfig.useSpacesForTab;
599 *pProvidedDefaults = true;
600 }
601
602 // extract num spaces for tab
603 it = dcfFields.find("NumSpacesForTab");
604 if (it != dcfFields.end())
605 {
606 if (!interpretIntValue(it->second, &(pConfig->numSpacesForTab)))
607 return requiredFieldError("NumSpacesForTab", pUserErrMsg);
608 }
609 else
610 {
611 pConfig->numSpacesForTab = defaultConfig.numSpacesForTab;
612 *pProvidedDefaults = true;
613 }
614
615 // extract auto append newline
616 it = dcfFields.find("AutoAppendNewline");
617 if (it != dcfFields.end())
618 {
619 if (!interpretBoolValue(it->second, &(pConfig->autoAppendNewline)))
620 return requiredFieldError("AutoAppendNewline", pUserErrMsg);
621 }
622 else
623 {
624 pConfig->autoAppendNewline = false;
625 }
626
627
628 // extract strip trailing whitespace
629 it = dcfFields.find("StripTrailingWhitespace");
630 if (it != dcfFields.end())
631 {
632 if (!interpretBoolValue(it->second, &(pConfig->stripTrailingWhitespace)))
633 return requiredFieldError("StripTrailingWhitespace", pUserErrMsg);
634 }
635 else
636 {
637 pConfig->stripTrailingWhitespace = false;
638 }
639
640 it = dcfFields.find("LineEndingConversion");
641 if (it != dcfFields.end())
642 {
643 if (!interpretLineEndingsValue(it->second, &(pConfig->lineEndings)))
644 return requiredFieldError("LineEndingConversion", pUserErrMsg);
645 }
646 else
647 {
648 pConfig->lineEndings = kLineEndingsUseDefault;
649 }
650
651
652 // extract encoding
653 it = dcfFields.find("Encoding");
654 if (it != dcfFields.end())
655 {
656 pConfig->encoding = it->second;
657 }
658 else
659 {
660 pConfig->encoding = defaultConfig.encoding;
661 *pProvidedDefaults = true;
662 }
663
664 // extract default sweave engine
665 it = dcfFields.find("RnwWeave");
666 if (it != dcfFields.end())
667 {
668 pConfig->defaultSweaveEngine = it->second;
669 }
670 else
671 {
672 pConfig->defaultSweaveEngine = defaultConfig.defaultSweaveEngine;
673 *pProvidedDefaults = true;
674 }
675
676 // extract default latex program
677 it = dcfFields.find("LaTeX");
678 if (it != dcfFields.end())
679 {
680 pConfig->defaultLatexProgram = it->second;
681 }
682 else
683 {
684 pConfig->defaultLatexProgram = defaultConfig.defaultLatexProgram;
685 *pProvidedDefaults = true;
686 }
687
688 // extract root document
689 it = dcfFields.find("RootDocument");
690 if (it != dcfFields.end())
691 {
692 pConfig->rootDocument = it->second;
693 }
694 else
695 {
696 pConfig->rootDocument = "";
697 }
698
699 // extract build type
700 it = dcfFields.find("BuildType");
701 if (it != dcfFields.end())
702 {
703 if (!interpretBuildTypeValue(it->second, &(pConfig->buildType)))
704 return requiredFieldError("BuildType", pUserErrMsg);
705 }
706 else
707 {
708 pConfig->buildType = defaultConfig.buildType;
709 }
710
711 // extract package path
712 it = dcfFields.find("PackagePath");
713 if (it != dcfFields.end())
714 {
715 pConfig->packagePath = it->second;
716 }
717 else
718 {
719 pConfig->packagePath = "";
720 }
721
722 // extract package install args
723 it = dcfFields.find("PackageInstallArgs");
724 if (it != dcfFields.end())
725 {
726 pConfig->packageInstallArgs = it->second;
727 }
728 else
729 {
730 pConfig->packageInstallArgs = "";
731 }
732
733 // extract package build args
734 it = dcfFields.find("PackageBuildArgs");
735 if (it != dcfFields.end())
736 {
737 pConfig->packageBuildArgs = it->second;
738 }
739 else
740 {
741 pConfig->packageBuildArgs = "";
742 }
743
744 // extract package build binary args
745 it = dcfFields.find("PackageBuildBinaryArgs");
746 if (it != dcfFields.end())
747 {
748 pConfig->packageBuildBinaryArgs = it->second;
749 }
750 else
751 {
752 pConfig->packageBuildBinaryArgs = "";
753 }
754
755 // extract package check args
756 it = dcfFields.find("PackageCheckArgs");
757 if (it != dcfFields.end())
758 {
759 pConfig->packageCheckArgs = it->second;
760 }
761 else
762 {
763 pConfig->packageCheckArgs = "";
764 }
765
766 // extract package roxygenzize
767 it = dcfFields.find("PackageRoxygenize");
768 if (it != dcfFields.end())
769 {
770 pConfig->packageRoxygenize = it->second;
771 }
772 else
773 {
774 pConfig->packageRoxygenize = "";
775 }
776
777 // extract package use devtools
778 it = dcfFields.find("PackageUseDevtools");
779 if (it != dcfFields.end())
780 {
781 if (!interpretBoolValue(it->second, &(pConfig->packageUseDevtools)))
782 return requiredFieldError("PackageUseDevtools", pUserErrMsg);
783 }
784 else
785 {
786 pConfig->packageUseDevtools = false;
787 }
788
789 // extract makefile path
790 it = dcfFields.find("MakefilePath");
791 if (it != dcfFields.end())
792 {
793 pConfig->makefilePath = it->second;
794 }
795 else
796 {
797 pConfig->makefilePath = "";
798 }
799
800 // extract websitepath
801 it = dcfFields.find("WebsitePath");
802 if (it != dcfFields.end())
803 {
804 pConfig->websitePath = it->second;
805 }
806 else
807 {
808 pConfig->websitePath = "";
809 }
810
811
812 // extract custom script path
813 it = dcfFields.find("CustomScriptPath");
814 if (it != dcfFields.end())
815 {
816 pConfig->customScriptPath = it->second;
817 }
818 else
819 {
820 pConfig->customScriptPath = "";
821 }
822
823 // auto-detect build type if necessary
824 if (pConfig->buildType.empty())
825 {
826 // try to detect the build type
827 pConfig->buildType = detectBuildType(projectFilePath,
828 buildDefaults,
829 pConfig);
830
831 // set *pProvidedDefaults only if we successfully auto-detected
832 // (this will prevent us from writing None into the project file,
833 // thus allowing auto-detection to work in the future if the user
834 // adds a DESCRIPTION or Makefile
835 if (pConfig->buildType != kBuildTypeNone)
836 *pProvidedDefaults = true;
837 }
838
839 // extract tutorial
840 it = dcfFields.find("Tutorial");
841 if (it != dcfFields.end())
842 {
843 pConfig->tutorialPath = it->second;
844 }
845 else
846 {
847 pConfig->tutorialPath = "";
848 }
849
850 // extract quit child processes on exit
851 it = dcfFields.find("QuitChildProcessesOnExit");
852 if (it != dcfFields.end())
853 {
854 if (!interpretYesNoAskValue(it->second, false, &(pConfig->quitChildProcessesOnExit)))
855 return requiredFieldError("QuitChildProcessesOnExit", pUserErrMsg);
856 }
857 else
858 {
859 pConfig->quitChildProcessesOnExit = defaultConfig.quitChildProcessesOnExit;
860 *pProvidedDefaults = true;
861 }
862
863 // extract execute rprofile
864 it = dcfFields.find("DisableExecuteRprofile");
865 if (it != dcfFields.end())
866 {
867 if (!interpretBoolValue(it->second, &(pConfig->disableExecuteRprofile)))
868 return requiredFieldError("DisableExecuteRprofile", pUserErrMsg);
869 }
870 else
871 {
872 pConfig->disableExecuteRprofile = false;
873 }
874
875 // extract default open docs
876 it = dcfFields.find("DefaultOpenDocs");
877 if (it != dcfFields.end())
878 {
879 pConfig->defaultOpenDocs = it->second;
880 }
881 else
882 {
883 pConfig->defaultOpenDocs = "";
884 }
885
886 // extract default tutorial
887 it = dcfFields.find("DefaultTutorial");
888 if (it != dcfFields.end())
889 {
890 pConfig->defaultTutorial = it->second;
891 }
892 else
893 {
894 pConfig->defaultTutorial = "";
895 }
896
897 // extract markdown wrap
898 it = dcfFields.find("MarkdownWrap");
899 if (it != dcfFields.end())
900 {
901 if (!interpretMarkdownWrapValue(it->second, &(pConfig->markdownWrap)))
902 return requiredFieldError("MarkdownWrap", pUserErrMsg);
903 }
904 else
905 {
906 pConfig->markdownWrap = defaultConfig.markdownWrap;
907 }
908
909 // extract markdown wrap at column
910 it = dcfFields.find("MarkdownWrapAtColumn");
911 if (it != dcfFields.end())
912 {
913 if (!interpretIntValue(it->second, &(pConfig->markdownWrapAtColumn)))
914 return requiredFieldError("MarkdownWrapAtColumn", pUserErrMsg);
915 }
916 else
917 {
918 pConfig->markdownWrapAtColumn = defaultConfig.markdownWrapAtColumn;
919 }
920
921 // extract markdown references
922 it = dcfFields.find("MarkdownReferences");
923 if (it != dcfFields.end())
924 {
925 if (!interpretMarkdownReferencesValue(it->second, &(pConfig->markdownReferences)))
926 return requiredFieldError("MarkdownReferences", pUserErrMsg);
927 }
928 else
929 {
930 pConfig->markdownReferences = defaultConfig.markdownReferences;
931 }
932
933 // extract markdown canonical
934 it = dcfFields.find("MarkdownCanonical");
935 if (it != dcfFields.end())
936 {
937 if (!interpretYesNoAskValue(it->second, false, &(pConfig->markdownCanonical)))
938 return requiredFieldError("MarkdownCanonical", pUserErrMsg);
939 }
940 else
941 {
942 pConfig->markdownCanonical = defaultConfig.markdownCanonical;
943 }
944
945 // extract zotero libraries
946 it = dcfFields.find("ZoteroLibraries");
947 if (it != dcfFields.end())
948 {
949 interpretZoteroLibraries(it->second, &(pConfig->zoteroLibraries));
950 }
951 else
952 {
953 pConfig->zoteroLibraries = defaultConfig.zoteroLibraries;
954 }
955
956 // extract python fields
957 it = dcfFields.find("PythonType");
958 if (it != dcfFields.end())
959 {
960 pConfig->pythonType = it->second;
961 }
962
963 it = dcfFields.find("PythonVersion");
964 if (it != dcfFields.end())
965 {
966 pConfig->pythonVersion = it->second;
967 }
968
969 it = dcfFields.find("PythonPath");
970 if (it != dcfFields.end())
971 {
972 pConfig->pythonPath = it->second;
973 }
974
975 // extract spelling fields
976 it = dcfFields.find("SpellingDictionary");
977 if (it != dcfFields.end())
978 {
979 pConfig->spellingDictionary = it->second;
980 }
981
982 return Success();
983 }
984
985
writeProjectFile(const FilePath & projectFilePath,const RProjectBuildDefaults & buildDefaults,const RProjectConfig & config)986 Error writeProjectFile(const FilePath& projectFilePath,
987 const RProjectBuildDefaults& buildDefaults,
988 const RProjectConfig& config)
989 {
990 // build version field if necessary
991 std::string rVersion;
992 if (!config.rVersion.isDefault())
993 rVersion = "RVersion: " + rVersionAsString(config.rVersion) + "\n\n";
994
995 // generate project file contents
996 boost::format fmt(
997 "Version: %1%\n"
998 "\n"
999 "%2%"
1000 "RestoreWorkspace: %3%\n"
1001 "SaveWorkspace: %4%\n"
1002 "AlwaysSaveHistory: %5%\n"
1003 "\n"
1004 "EnableCodeIndexing: %6%\n"
1005 "UseSpacesForTab: %7%\n"
1006 "NumSpacesForTab: %8%\n"
1007 "Encoding: %9%\n"
1008 "\n"
1009 "RnwWeave: %10%\n"
1010 "LaTeX: %11%\n");
1011
1012 std::string contents = boost::str(fmt %
1013 boost::io::group(std::fixed, std::setprecision(1), config.version) %
1014 rVersion %
1015 yesNoAskValueToString(config.restoreWorkspace) %
1016 yesNoAskValueToString(config.saveWorkspace) %
1017 yesNoAskValueToString(config.alwaysSaveHistory) %
1018 boolValueToString(config.enableCodeIndexing) %
1019 boolValueToString(config.useSpacesForTab) %
1020 config.numSpacesForTab %
1021 config.encoding %
1022 config.defaultSweaveEngine %
1023 config.defaultLatexProgram);
1024
1025 // add root-document if provided
1026 if (!config.rootDocument.empty())
1027 {
1028 boost::format rootDocFmt("RootDocument: %1%\n");
1029 std::string rootDoc = boost::str(rootDocFmt % config.rootDocument);
1030 contents.append(rootDoc);
1031 }
1032
1033 // additional editor settings
1034 if (config.autoAppendNewline || config.stripTrailingWhitespace ||
1035 (config.lineEndings != kLineEndingsUseDefault) )
1036 {
1037 contents.append("\n");
1038
1039 if (config.autoAppendNewline)
1040 {
1041 contents.append("AutoAppendNewline: Yes\n");
1042 }
1043
1044 if (config.stripTrailingWhitespace)
1045 {
1046 contents.append("StripTrailingWhitespace: Yes\n");
1047 }
1048
1049 if (config.lineEndings != kLineEndingsUseDefault)
1050 {
1051 std::string value;
1052 switch(config.lineEndings)
1053 {
1054 case string_utils::LineEndingPassthrough:
1055 value = kLineEndingPassthough;
1056 break;
1057 case string_utils::LineEndingNative:
1058 value = kLineEndingNative;
1059 break;
1060 case string_utils::LineEndingPosix:
1061 value = kLineEndingPosix;
1062 break;
1063 case string_utils::LineEndingWindows:
1064 value = kLineEndingWindows;
1065 break;
1066 }
1067 if (!value.empty())
1068 contents.append("LineEndingConversion: " + value + "\n");
1069 }
1070 }
1071
1072 // add build-specific settings if necessary
1073 if (!config.buildType.empty())
1074 {
1075 // if the build type is None and the detected build type is None
1076 // then don't write any build type into the file (so that auto-detection
1077 // has a chance to work in the future if the user turns this project
1078 // into a package or adds a Makefile)
1079 if (config.buildType != kBuildTypeNone ||
1080 detectBuildType(projectFilePath, buildDefaults) != kBuildTypeNone)
1081 {
1082 // build type
1083 boost::format buildFmt("\nBuildType: %1%\n");
1084 std::string build = boost::str(buildFmt % config.buildType);
1085
1086 // extra fields
1087 if (config.buildType == kBuildTypePackage)
1088 {
1089 if (config.packageUseDevtools)
1090 {
1091 build.append("PackageUseDevtools: Yes\n");
1092 }
1093
1094 if (!config.packagePath.empty())
1095 {
1096 boost::format pkgFmt("PackagePath: %1%\n");
1097 build.append(boost::str(pkgFmt % config.packagePath));
1098 }
1099
1100 if (!config.packageInstallArgs.empty())
1101 {
1102 boost::format pkgFmt("PackageInstallArgs: %1%\n");
1103 build.append(boost::str(pkgFmt % config.packageInstallArgs));
1104 }
1105
1106 if (!config.packageBuildArgs.empty())
1107 {
1108 boost::format pkgFmt("PackageBuildArgs: %1%\n");
1109 build.append(boost::str(pkgFmt % config.packageBuildArgs));
1110 }
1111
1112 if (!config.packageBuildBinaryArgs.empty())
1113 {
1114 boost::format pkgFmt("PackageBuildBinaryArgs: %1%\n");
1115 build.append(boost::str(pkgFmt % config.packageBuildBinaryArgs));
1116 }
1117
1118 if (!config.packageCheckArgs.empty())
1119 {
1120 boost::format pkgFmt("PackageCheckArgs: %1%\n");
1121 build.append(boost::str(pkgFmt % config.packageCheckArgs));
1122 }
1123
1124 if (!config.packageRoxygenize.empty())
1125 {
1126 boost::format pkgFmt("PackageRoxygenize: %1%\n");
1127 build.append(boost::str(pkgFmt % config.packageRoxygenize));
1128 }
1129
1130 }
1131 else if (config.buildType == kBuildTypeMakefile)
1132 {
1133 if (!config.makefilePath.empty())
1134 {
1135 boost::format makefileFmt("MakefilePath: %1%\n");
1136 build.append(boost::str(makefileFmt % config.makefilePath));
1137 }
1138 }
1139 else if (config.buildType == kBuildTypeWebsite)
1140 {
1141 if (!config.websitePath.empty())
1142 {
1143 boost::format websiteFmt("WebsitePath: %1%\n");
1144 build.append(boost::str(websiteFmt % config.websitePath));
1145 }
1146 }
1147 else if (config.buildType == kBuildTypeCustom)
1148 {
1149 boost::format customFmt("CustomScriptPath: %1%\n");
1150 build.append(boost::str(customFmt % config.customScriptPath));
1151 }
1152
1153 // add to contents
1154 contents.append(build);
1155 }
1156 }
1157
1158 // add Tutorial if it's present
1159 if (!config.tutorialPath.empty())
1160 {
1161 boost::format tutorialFmt("\nTutorial: %1%\n");
1162 contents.append(boost::str(tutorialFmt % config.tutorialPath));
1163 }
1164
1165 // add QuitChildProcessesOnExit if it's not the default
1166 if (config.quitChildProcessesOnExit != DefaultValue)
1167 {
1168 boost::format quitChildProcFmt("\nQuitChildProcessesOnExit: %1%\n");
1169 contents.append(boost::str(quitChildProcFmt % yesNoAskValueToString(config.quitChildProcessesOnExit)));
1170 }
1171
1172 // add DisableExecuteRprofile if it's not the default
1173 if (config.disableExecuteRprofile)
1174 {
1175 contents.append("DisableExecuteRprofile: Yes\n");
1176 }
1177
1178 // add default open docs if it's present
1179 if (!config.defaultOpenDocs.empty())
1180 {
1181 boost::format docsFmt("\nDefaultOpenDocs: %1%\n");
1182 contents.append(boost::str(docsFmt % config.defaultOpenDocs));
1183 }
1184
1185 // add default tutorial if present
1186 if (!config.defaultTutorial.empty())
1187 {
1188 boost::format docsFmt("\nDefaultTutorial: %1%\n");
1189 contents.append(boost::str(docsFmt % config.defaultTutorial));
1190 }
1191
1192 // if any markdown configs deviate from the default then create a markdown section
1193 if (config.markdownWrap != kMarkdownWrapUseDefault ||
1194 config.markdownReferences != kMarkdownReferencesUseDefault ||
1195 config.markdownCanonical != DefaultValue)
1196 {
1197 contents.append("\n");
1198
1199 if (config.markdownWrap != kMarkdownWrapUseDefault)
1200 {
1201 boost::format fmt("MarkdownWrap: %1%\n");
1202 contents.append(boost::str(fmt % config.markdownWrap));
1203 if (config.markdownWrap == kMarkdownWrapColumn)
1204 {
1205 boost::format fmt("MarkdownWrapAtColumn: %1%\n");
1206 contents.append(boost::str(fmt % config.markdownWrapAtColumn));
1207 }
1208 }
1209
1210 if (config.markdownReferences != kMarkdownReferencesUseDefault)
1211 {
1212 boost::format fmt("MarkdownReferences: %1%\n");
1213 contents.append(boost::str(fmt % config.markdownReferences));
1214 }
1215
1216 if (config.markdownCanonical != DefaultValue)
1217 {
1218 boost::format fmt("MarkdownCanonical: %1%\n");
1219 contents.append(boost::str(fmt % yesNoAskValueToString(config.markdownCanonical)));
1220 }
1221 }
1222
1223 // if we have zotero config create a zotero section
1224 if (config.zoteroLibraries.has_value() && !config.zoteroLibraries.get().empty())
1225 {
1226 auto libraries = config.zoteroLibraries.get();
1227 std::string librariesConfig = text::encodeCsvLine(libraries);
1228 boost::format fmt("\nZoteroLibraries: %1%\n");
1229 contents.append(boost::str(fmt % librariesConfig));
1230 }
1231
1232 // if any Python configs deviate from default, then create Python section
1233 if (!config.pythonType.empty() ||
1234 !config.pythonVersion.empty() ||
1235 !config.pythonPath.empty())
1236 {
1237 boost::format fmt(
1238 "\n"
1239 "PythonType: %1%\n"
1240 "PythonVersion: %2%\n"
1241 "PythonPath: %3%\n");
1242
1243 auto pythonConfig = fmt
1244 % config.pythonType
1245 % config.pythonVersion
1246 % config.pythonPath;
1247
1248 contents.append(boost::str(pythonConfig));
1249 }
1250
1251 // add spelling dictionary if present
1252 if (!config.spellingDictionary.empty())
1253 {
1254 boost::format fmt("\nSpellingDictionary: %1%\n");
1255 contents.append(boost::str(fmt % config.spellingDictionary));
1256 }
1257
1258
1259 // write it
1260 return writeStringToFile(projectFilePath,
1261 contents,
1262 string_utils::LineEndingNative);
1263 }
1264
projectFromDirectory(const FilePath & path)1265 FilePath projectFromDirectory(const FilePath& path)
1266 {
1267 // canonicalize the path; this handles the case (among others) where the incoming
1268 // path ends with a "/"; without removing that, the matching logic below fails
1269 FilePath directoryPath(path.getCanonicalPath());
1270
1271 // first use simple heuristic of a case sentitive match between
1272 // directory name and project file name
1273 std::string dirName = directoryPath.getFilename();
1274 if (!FilePath::isRootPath(dirName))
1275 {
1276 FilePath projectFile = directoryPath.completeChildPath(dirName + ".Rproj");
1277 if (projectFile.exists())
1278 return projectFile;
1279 }
1280
1281 // didn't satisfy it with simple check so do scan of directory
1282 std::vector<FilePath> children;
1283 Error error = directoryPath.getChildren(children);
1284 if (error)
1285 {
1286 LOG_ERROR(error);
1287 return FilePath();
1288 }
1289
1290 // build a vector of children with .rproj extensions. at the same
1291 // time allow for a case insensitive match with dir name and return that
1292 std::string projFileLower = string_utils::toLower(dirName);
1293 std::vector<FilePath> rprojFiles;
1294 for (std::vector<FilePath>::const_iterator it = children.begin();
1295 it != children.end();
1296 ++it)
1297 {
1298 if (!it->isDirectory() && (it->getExtensionLowerCase() == ".rproj"))
1299 {
1300 if (string_utils::toLower(it->getFilename()) == projFileLower)
1301 return *it;
1302 else
1303 rprojFiles.push_back(*it);
1304 }
1305 }
1306
1307 // if we found only one rproj file then return it
1308 if (rprojFiles.size() == 1)
1309 {
1310 return rprojFiles.at(0);
1311 }
1312 // more than one, take most recent
1313 else if (rprojFiles.size() > 1 )
1314 {
1315 FilePath projectFile = rprojFiles.at(0);
1316 for (std::vector<FilePath>::const_iterator it = rprojFiles.begin();
1317 it != rprojFiles.end();
1318 ++it)
1319 {
1320 if (it->getLastWriteTime() > projectFile.getLastWriteTime())
1321 projectFile = *it;
1322 }
1323
1324 return projectFile;
1325 }
1326 // didn't find one
1327 else
1328 {
1329 return FilePath();
1330 }
1331 }
1332
updateSetPackageInstallArgsDefault(RProjectConfig * pConfig)1333 bool updateSetPackageInstallArgsDefault(RProjectConfig* pConfig)
1334 {
1335 if (pConfig->packageInstallArgs == kPackageInstallArgsPreviousDefault)
1336 {
1337 pConfig->packageInstallArgs = kPackageInstallArgsDefault;
1338 return true;
1339 }
1340 else
1341 {
1342 return false;
1343 }
1344 }
1345
isWebsiteDirectory(const FilePath & projectDir)1346 bool isWebsiteDirectory(const FilePath& projectDir)
1347 {
1348 // look for an index.Rmd or index.md
1349 FilePath indexFile = projectDir.completeChildPath("index.Rmd");
1350 if (!indexFile.exists())
1351 indexFile = projectDir.completeChildPath("index.md");
1352 if (indexFile.exists())
1353 {
1354 // look for _site.yml
1355 FilePath siteFile = projectDir.completeChildPath("_site.yml");
1356 if (siteFile.exists())
1357 {
1358 return true;
1359 }
1360 // no _site.yml, is there a custom site generator?
1361 else
1362 {
1363 static const boost::regex reSite("^site:.*$");
1364 std::string yaml = yaml::extractYamlHeader(indexFile);
1365 return regex_utils::search(yaml.begin(), yaml.end(), reSite);
1366 }
1367 }
1368 else
1369 {
1370 return false;
1371 }
1372 }
1373
1374 // find the website root (if any) for the given filepath
websiteRootDirectory(const FilePath & filePath)1375 FilePath websiteRootDirectory(const FilePath& filePath)
1376 {
1377 FilePath dir = filePath.getParent();
1378 while (!dir.isEmpty())
1379 {
1380 if (r_util::isWebsiteDirectory(dir))
1381 return dir;
1382
1383 dir = dir.getParent();
1384 }
1385
1386 return FilePath();
1387 }
1388
1389 } // namespace r_util
1390 } // namespace core
1391 } // namespace rstudio
1392
1393
1394
1395