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