1 /*
2   ==============================================================================
3 
4    This file is part of the JUCE library.
5    Copyright (c) 2020 - Raw Material Software Limited
6 
7    JUCE is an open source library subject to commercial or open-source
8    licensing.
9 
10    By using JUCE, you agree to the terms of both the JUCE 6 End-User License
11    Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020).
12 
13    End User License Agreement: www.juce.com/juce-6-licence
14    Privacy Policy: www.juce.com/juce-privacy-policy
15 
16    Or: You may also use this code under the terms of the GPL v3 (see
17    www.gnu.org/licenses).
18 
19    JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
20    EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
21    DISCLAIMED.
22 
23   ==============================================================================
24 */
25 
26 #include "jucer_ProjectSaver.h"
27 #include "jucer_ProjectExport_CLion.h"
28 #include "../Application/jucer_Application.h"
29 
30 static constexpr const char* generatedGroupID = "__jucelibfiles";
31 static constexpr const char* generatedGroupUID = "__generatedcode__";
32 
33 constexpr int jucerFormatVersion = 1;
34 
35 //==============================================================================
ProjectSaver(Project & p)36 ProjectSaver::ProjectSaver (Project& p)
37     : project (p),
38       generatedCodeFolder (project.getGeneratedCodeFolder()),
39       generatedFilesGroup (Project::Item::createGroup (project, getJuceCodeGroupName(), generatedGroupUID, true)),
40       projectLineFeed (project.getProjectLineFeed())
41 {
42     generatedFilesGroup.setID (generatedGroupID);
43 }
44 
save(ProjectExporter * exporterToSave)45 Result ProjectSaver::save (ProjectExporter* exporterToSave)
46 {
47     if (! ProjucerApplication::getApp().isRunningCommandLine)
48     {
49         SaveThreadWithProgressWindow thread (*this, exporterToSave);
50         thread.runThread();
51 
52         return thread.result;
53     }
54 
55     return saveProject (exporterToSave);
56 }
57 
saveResourcesOnly()58 Result ProjectSaver::saveResourcesOnly()
59 {
60     writeBinaryDataFiles();
61 
62     if (! errors.isEmpty())
63         return Result::fail (errors[0]);
64 
65     return Result::ok();
66 }
67 
saveBasicProjectItems(const OwnedArray<LibraryModule> & modules,const String & appConfigUserContent)68 void ProjectSaver::saveBasicProjectItems (const OwnedArray<LibraryModule>& modules, const String& appConfigUserContent)
69 {
70     writePluginDefines();
71     writeAppConfigFile (modules, appConfigUserContent);
72     writeBinaryDataFiles();
73     writeAppHeader (modules);
74     writeModuleCppWrappers (modules);
75 }
76 
saveContentNeededForLiveBuild()77 Result ProjectSaver::saveContentNeededForLiveBuild()
78 {
79     auto modules = getModules();
80 
81     if (errors.isEmpty())
82     {
83         saveBasicProjectItems (modules, loadUserContentFromAppConfig());
84         return Result::ok();
85     }
86 
87     return Result::fail (errors[0]);
88 }
89 
addFileToGeneratedGroup(const File & file)90 Project::Item ProjectSaver::addFileToGeneratedGroup (const File& file)
91 {
92     auto item = generatedFilesGroup.findItemForFile (file);
93 
94     if (item.isValid())
95         return item;
96 
97     generatedFilesGroup.addFileAtIndex (file, -1, true);
98     return generatedFilesGroup.findItemForFile (file);
99 }
100 
copyFolder(const File & source,const File & dest)101 bool ProjectSaver::copyFolder (const File& source, const File& dest)
102 {
103     if (source.isDirectory() && dest.createDirectory())
104     {
105         for (auto& f : source.findChildFiles (File::findFiles, false))
106         {
107             auto target = dest.getChildFile (f.getFileName());
108             filesCreated.add (target);
109 
110             if (! f.copyFileTo (target))
111                 return false;
112         }
113 
114         for (auto& f : source.findChildFiles (File::findDirectories, false))
115         {
116             auto name = f.getFileName();
117 
118             if (name == ".git" || name == ".svn" || name == ".cvs")
119                 continue;
120 
121             if (! copyFolder (f, dest.getChildFile (f.getFileName())))
122                 return false;
123         }
124 
125         return true;
126     }
127 
128     return false;
129 }
130 
131 //==============================================================================
saveGeneratedFile(const String & filePath,const MemoryOutputStream & newData)132 Project::Item ProjectSaver::saveGeneratedFile (const String& filePath, const MemoryOutputStream& newData)
133 {
134     if (! generatedCodeFolder.createDirectory())
135     {
136         addError ("Couldn't create folder: " + generatedCodeFolder.getFullPathName());
137         return Project::Item (project, {}, false);
138     }
139 
140     auto file = generatedCodeFolder.getChildFile (filePath);
141 
142     if (replaceFileIfDifferent (file, newData))
143         return addFileToGeneratedGroup (file);
144 
145     return { project, {}, true };
146 }
147 
replaceFileIfDifferent(const File & f,const MemoryOutputStream & newData)148 bool ProjectSaver::replaceFileIfDifferent (const File& f, const MemoryOutputStream& newData)
149 {
150     filesCreated.add (f);
151 
152     if (! build_tools::overwriteFileWithNewDataIfDifferent (f, newData))
153     {
154         addError ("Can't write to file: " + f.getFullPathName());
155         return false;
156     }
157 
158     return true;
159 }
160 
deleteUnwantedFilesIn(const File & parent)161 bool ProjectSaver::deleteUnwantedFilesIn (const File& parent)
162 {
163     // Recursively clears out any files in a folder that we didn't create, but avoids
164     // any folders containing hidden files that might be used by version-control systems.
165     auto shouldFileBeKept = [] (const String& filename)
166     {
167         static StringArray filesToKeep (".svn", ".cvs", "CMakeLists.txt");
168         return filesToKeep.contains (filename);
169     };
170 
171     bool folderIsNowEmpty = true;
172     Array<File> filesToDelete;
173 
174     for (const auto& i : RangedDirectoryIterator (parent, false, "*", File::findFilesAndDirectories))
175     {
176         auto f = i.getFile();
177 
178         if (filesCreated.contains (f) || shouldFileBeKept (f.getFileName()))
179         {
180             folderIsNowEmpty = false;
181         }
182         else if (i.isDirectory())
183         {
184             if (deleteUnwantedFilesIn (f))
185                 filesToDelete.add (f);
186             else
187                 folderIsNowEmpty = false;
188         }
189         else
190         {
191             filesToDelete.add (f);
192         }
193     }
194 
195     for (int j = filesToDelete.size(); --j >= 0;)
196         filesToDelete.getReference (j).deleteRecursively();
197 
198     return folderIsNowEmpty;
199 }
200 
201 //==============================================================================
addError(const String & message)202 void ProjectSaver::addError (const String& message)
203 {
204     const ScopedLock sl (errorLock);
205     errors.add (message);
206 }
207 
208 //==============================================================================
getAppConfigFile() const209 File ProjectSaver::getAppConfigFile() const
210 {
211     return generatedCodeFolder.getChildFile (Project::getAppConfigFilename());
212 }
213 
getPluginDefinesFile() const214 File ProjectSaver::getPluginDefinesFile() const
215 {
216     return generatedCodeFolder.getChildFile (Project::getPluginDefinesFilename());
217 }
218 
loadUserContentFromAppConfig() const219 String ProjectSaver::loadUserContentFromAppConfig() const
220 {
221     StringArray userContent;
222     bool foundCodeSection = false;
223     auto lines = StringArray::fromLines (getAppConfigFile().loadFileAsString());
224 
225     for (int i = 0; i < lines.size(); ++i)
226     {
227         if (lines[i].contains ("[BEGIN_USER_CODE_SECTION]"))
228         {
229             for (int j = i + 1; j < lines.size() && ! lines[j].contains ("[END_USER_CODE_SECTION]"); ++j)
230                 userContent.add (lines[j]);
231 
232             foundCodeSection = true;
233             break;
234         }
235     }
236 
237     if (! foundCodeSection)
238     {
239         userContent.add ({});
240         userContent.add ("// (You can add your own code in this section, and the Projucer will not overwrite it)");
241         userContent.add ({});
242     }
243 
244     return userContent.joinIntoString (projectLineFeed) + projectLineFeed;
245 }
246 
247 //==============================================================================
getModules()248 OwnedArray<LibraryModule> ProjectSaver::getModules()
249 {
250     OwnedArray<LibraryModule> modules;
251     project.getEnabledModules().createRequiredModules (modules);
252 
253     auto isCommandLine = ProjucerApplication::getApp().isRunningCommandLine;
254 
255     for (auto* module : modules)
256     {
257         if (! module->isValid())
258         {
259             addError (String ("At least one of your JUCE module paths is invalid!\n")
260                 + (isCommandLine ? "Please ensure each module path points to the correct JUCE modules folder."
261                                  : "Please go to the Modules settings page and ensure each path points to the correct JUCE modules folder."));
262 
263             return {};
264         }
265 
266         if (project.getEnabledModules().getExtraDependenciesNeeded (module->getID()).size() > 0)
267         {
268             addError (String ("At least one of your modules has missing dependencies!\n")
269                 + (isCommandLine ? "Please add the required dependencies, or run the command again with the \"--fix-missing-dependencies\" option."
270                                  : "Please go to the settings page of the highlighted modules and add the required dependencies."));
271 
272             return {};
273         }
274     }
275 
276     return modules;
277 }
278 
279 //==============================================================================
saveProject(ProjectExporter * specifiedExporterToSave)280 Result ProjectSaver::saveProject (ProjectExporter* specifiedExporterToSave)
281 {
282     if (project.getNumExporters() == 0)
283     {
284         return Result::fail ("No exporters found!\n"
285                              "Please add an exporter before saving.");
286     }
287 
288     auto oldProjectFile = project.getFile();
289     auto modules = getModules();
290 
291     if (errors.isEmpty())
292     {
293         if (project.isAudioPluginProject())
294         {
295             if (project.shouldBuildUnityPlugin())
296                 writeUnityScriptFile();
297         }
298 
299         saveBasicProjectItems (modules, loadUserContentFromAppConfig());
300         writeProjects (modules, specifiedExporterToSave);
301         writeProjectFile();
302 
303         runPostExportScript();
304 
305         if (generatedCodeFolder.exists())
306         {
307             writeReadmeFile();
308             deleteUnwantedFilesIn (generatedCodeFolder);
309         }
310 
311         if (errors.isEmpty())
312             return Result::ok();
313     }
314 
315     project.setFile (oldProjectFile);
316     return Result::fail (errors[0]);
317 }
318 
319 //==============================================================================
writePluginDefines(MemoryOutputStream & out) const320 void ProjectSaver::writePluginDefines (MemoryOutputStream& out) const
321 {
322     const auto pluginDefines = getAudioPluginDefines();
323 
324     if (pluginDefines.isEmpty())
325         return;
326 
327     writeAutoGenWarningComment (out);
328 
329     out << "*/" << newLine << newLine
330         << "#pragma once" << newLine << newLine
331         << pluginDefines << newLine;
332 }
333 
writeProjectFile()334 void ProjectSaver::writeProjectFile()
335 {
336     auto root = project.getProjectRoot();
337 
338     root.removeProperty ("jucerVersion", nullptr);
339 
340     if ((int) root.getProperty (Ids::jucerFormatVersion, -1) != jucerFormatVersion)
341         root.setProperty (Ids::jucerFormatVersion, jucerFormatVersion, nullptr);
342 
343     project.updateCachedFileState();
344 
345     auto newSerialisedXml = project.serialiseProjectXml (root.createXml());
346     jassert (newSerialisedXml.isNotEmpty());
347 
348     if (newSerialisedXml != project.getCachedFileStateContent())
349     {
350         project.getFile().replaceWithText (newSerialisedXml);
351         project.updateCachedFileState();
352     }
353 }
354 
writeAppConfig(MemoryOutputStream & out,const OwnedArray<LibraryModule> & modules,const String & userContent)355 void ProjectSaver::writeAppConfig (MemoryOutputStream& out, const OwnedArray<LibraryModule>& modules, const String& userContent)
356 {
357     if (! project.shouldUseAppConfig())
358         return;
359 
360     writeAutoGenWarningComment (out);
361 
362     out << "    There's a section below where you can add your own custom code safely, and the" << newLine
363         << "    Projucer will preserve the contents of that block, but the best way to change" << newLine
364         << "    any of these definitions is by using the Projucer's project settings." << newLine
365         << newLine
366         << "    Any commented-out settings will assume their default values." << newLine
367         << newLine
368         << "*/" << newLine
369         << newLine;
370 
371     out << "#pragma once" << newLine
372         << newLine
373         << "//==============================================================================" << newLine
374         << "// [BEGIN_USER_CODE_SECTION]" << newLine
375         << userContent
376         << "// [END_USER_CODE_SECTION]" << newLine;
377 
378     if (getPluginDefinesFile().existsAsFile() && getAudioPluginDefines().isNotEmpty())
379         out << newLine << CodeHelpers::createIncludeStatement (Project::getPluginDefinesFilename()) << newLine;
380 
381     out << newLine
382         << "/*" << newLine
383         << "  ==============================================================================" << newLine
384         << newLine
385         << "   In accordance with the terms of the JUCE 6 End-Use License Agreement, the" << newLine
386         << "   JUCE Code in SECTION A cannot be removed, changed or otherwise rendered" << newLine
387         << "   ineffective unless you have a JUCE Indie or Pro license, or are using JUCE" << newLine
388         << "   under the GPL v3 license." << newLine
389         << newLine
390         << "   End User License Agreement: www.juce.com/juce-6-licence" << newLine
391         << newLine
392         << "  ==============================================================================" << newLine
393         << "*/" << newLine
394         << newLine
395         << "// BEGIN SECTION A" << newLine
396         << newLine
397         << "#ifndef JUCE_DISPLAY_SPLASH_SCREEN" << newLine
398         << " #define JUCE_DISPLAY_SPLASH_SCREEN "   << (project.shouldDisplaySplashScreen() ? "1" : "0") << newLine
399         << "#endif" << newLine << newLine
400         << "// END SECTION A" << newLine
401         << newLine
402         << "#define JUCE_USE_DARK_SPLASH_SCREEN "  << (project.getSplashScreenColourString() == "Dark" ? "1" : "0") << newLine
403         << newLine
404         << "#define JUCE_PROJUCER_VERSION 0x" << String::toHexString (ProjectInfo::versionNumber) << newLine;
405 
406     out << newLine
407         << "//==============================================================================" << newLine;
408 
409     auto longestModuleName = [&modules]()
410     {
411         int longest = 0;
412 
413         for (auto* module : modules)
414             longest = jmax (longest, module->getID().length());
415 
416         return longest;
417     }();
418 
419     for (auto* module : modules)
420     {
421         out << "#define JUCE_MODULE_AVAILABLE_" << module->getID()
422             << String::repeatedString (" ", longestModuleName + 5 - module->getID().length()) << " 1" << newLine;
423     }
424 
425     out << newLine << "#define JUCE_GLOBAL_MODULE_SETTINGS_INCLUDED 1" << newLine;
426 
427     for (auto* module : modules)
428     {
429         OwnedArray<Project::ConfigFlag> flags;
430         module->getConfigFlags (project, flags);
431 
432         if (flags.size() > 0)
433         {
434             out << newLine
435                 << "//==============================================================================" << newLine
436                 << "// " << module->getID() << " flags:" << newLine;
437 
438             for (auto* flag : flags)
439             {
440                 out << newLine
441                 << "#ifndef    " << flag->symbol
442                 << newLine
443                 << (flag->value.isUsingDefault() ? " //#define " : " #define   ") << flag->symbol << " " << (flag->value.get() ? "1" : "0")
444                 << newLine
445                 << "#endif"
446                 << newLine;
447             }
448         }
449     }
450 
451     auto& type = project.getProjectType();
452     auto isStandaloneApplication = (! type.isAudioPlugin() && ! type.isDynamicLibrary());
453 
454     out << newLine
455         << "//==============================================================================" << newLine
456         << "#ifndef    JUCE_STANDALONE_APPLICATION" << newLine
457         << " #if defined(JucePlugin_Name) && defined(JucePlugin_Build_Standalone)" << newLine
458         << "  #define  JUCE_STANDALONE_APPLICATION JucePlugin_Build_Standalone" << newLine
459         << " #else" << newLine
460         << "  #define  JUCE_STANDALONE_APPLICATION " << (isStandaloneApplication ? "1" : "0") << newLine
461         << " #endif" << newLine
462         << "#endif" << newLine;
463 }
464 
465 template <typename WriterCallback>
writeOrRemoveGeneratedFile(const String & name,WriterCallback && writerCallback)466 void ProjectSaver::writeOrRemoveGeneratedFile (const String& name, WriterCallback&& writerCallback)
467 {
468     MemoryOutputStream mem;
469     mem.setNewLineString (projectLineFeed);
470 
471     writerCallback (mem);
472 
473     if (mem.getDataSize() != 0)
474     {
475         saveGeneratedFile (name, mem);
476         return;
477     }
478 
479     const auto destFile = generatedCodeFolder.getChildFile (name);
480 
481     if (destFile.existsAsFile())
482     {
483         if (! destFile.deleteFile())
484             addError ("Couldn't remove unnecessary file: " + destFile.getFullPathName());
485     }
486 }
487 
writePluginDefines()488 void ProjectSaver::writePluginDefines()
489 {
490     writeOrRemoveGeneratedFile (Project::getPluginDefinesFilename(), [&] (MemoryOutputStream& mem)
491     {
492         writePluginDefines (mem);
493     });
494 }
495 
writeAppConfigFile(const OwnedArray<LibraryModule> & modules,const String & userContent)496 void ProjectSaver::writeAppConfigFile (const OwnedArray<LibraryModule>& modules, const String& userContent)
497 {
498     writeOrRemoveGeneratedFile (Project::getAppConfigFilename(), [&] (MemoryOutputStream& mem)
499     {
500         writeAppConfig (mem, modules, userContent);
501     });
502 }
503 
writeAppHeader(MemoryOutputStream & out,const OwnedArray<LibraryModule> & modules)504 void ProjectSaver::writeAppHeader (MemoryOutputStream& out, const OwnedArray<LibraryModule>& modules)
505 {
506     writeAutoGenWarningComment (out);
507 
508     out << "    This is the header file that your files should include in order to get all the" << newLine
509         << "    JUCE library headers. You should avoid including the JUCE headers directly in" << newLine
510         << "    your own source files, because that wouldn't pick up the correct configuration" << newLine
511         << "    options for your app." << newLine
512         << newLine
513         << "*/" << newLine << newLine;
514 
515     out << "#pragma once" << newLine << newLine;
516 
517     if (getAppConfigFile().exists() && project.shouldUseAppConfig())
518         out << CodeHelpers::createIncludeStatement (Project::getAppConfigFilename()) << newLine;
519 
520     if (modules.size() > 0)
521     {
522         out << newLine;
523 
524         for (auto* module : modules)
525             module->writeIncludes (*this, out);
526 
527         out << newLine;
528     }
529 
530     if (hasBinaryData && project.shouldIncludeBinaryInJuceHeader())
531         out << CodeHelpers::createIncludeStatement (project.getBinaryDataHeaderFile(), getAppConfigFile()) << newLine;
532 
533     out << newLine
534         << "#if defined (JUCE_PROJUCER_VERSION) && JUCE_PROJUCER_VERSION < JUCE_VERSION" << newLine
535         << " /** If you've hit this error then the version of the Projucer that was used to generate this project is" << newLine
536         << "     older than the version of the JUCE modules being included. To fix this error, re-save your project" << newLine
537         << "     using the latest version of the Projucer or, if you aren't using the Projucer to manage your project," << newLine
538         << "     remove the JUCE_PROJUCER_VERSION define." << newLine
539         << " */" << newLine
540         << " #error \"This project was last saved using an outdated version of the Projucer! Re-save this project with the latest version to fix this error.\"" << newLine
541         << "#endif" << newLine
542         << newLine;
543 
544     if (project.shouldAddUsingNamespaceToJuceHeader())
545         out << "#if ! DONT_SET_USING_JUCE_NAMESPACE" << newLine
546             << " // If your code uses a lot of JUCE classes, then this will obviously save you" << newLine
547             << " // a lot of typing, but can be disabled by setting DONT_SET_USING_JUCE_NAMESPACE." << newLine
548             << " using namespace juce;" << newLine
549             << "#endif" << newLine;
550 
551     out << newLine
552         << "#if ! JUCE_DONT_DECLARE_PROJECTINFO" << newLine
553         << "namespace ProjectInfo" << newLine
554         << "{" << newLine
555         << "    const char* const  projectName    = " << CppTokeniserFunctions::addEscapeChars (project.getProjectNameString()).quoted() << ";" << newLine
556         << "    const char* const  companyName    = " << CppTokeniserFunctions::addEscapeChars (project.getCompanyNameString()).quoted() << ";" << newLine
557         << "    const char* const  versionString  = " << CppTokeniserFunctions::addEscapeChars (project.getVersionString()).quoted() << ";" << newLine
558         << "    const int          versionNumber  = " << project.getVersionAsHex() << ";" << newLine
559         << "}" << newLine
560         << "#endif" << newLine;
561 }
562 
writeAppHeader(const OwnedArray<LibraryModule> & modules)563 void ProjectSaver::writeAppHeader (const OwnedArray<LibraryModule>& modules)
564 {
565     MemoryOutputStream mem;
566     mem.setNewLineString (projectLineFeed);
567 
568     writeAppHeader (mem, modules);
569     saveGeneratedFile (Project::getJuceSourceHFilename(), mem);
570 }
571 
writeModuleCppWrappers(const OwnedArray<LibraryModule> & modules)572 void ProjectSaver::writeModuleCppWrappers (const OwnedArray<LibraryModule>& modules)
573 {
574     for (auto* module : modules)
575     {
576         for (auto& cu : module->getAllCompileUnits())
577         {
578             MemoryOutputStream mem;
579             mem.setNewLineString (projectLineFeed);
580 
581             writeAutoGenWarningComment (mem);
582 
583             mem << "*/" << newLine << newLine;
584 
585             if (project.shouldUseAppConfig())
586                 mem << "#include " << Project::getAppConfigFilename().quoted() << newLine;
587 
588             mem << "#include <";
589 
590             if (cu.file.getFileExtension() != ".r")   // .r files are included without the path
591                 mem << module->getID() << "/";
592 
593             mem << cu.file.getFileName() << ">" << newLine;
594 
595             replaceFileIfDifferent (generatedCodeFolder.getChildFile (cu.getFilenameForProxyFile()), mem);
596         }
597     }
598 }
599 
writeBinaryDataFiles()600 void ProjectSaver::writeBinaryDataFiles()
601 {
602     auto binaryDataH = project.getBinaryDataHeaderFile();
603 
604     JucerResourceFile resourceFile (project);
605 
606     if (resourceFile.getNumFiles() > 0)
607     {
608         auto dataNamespace = project.getBinaryDataNamespaceString().trim();
609 
610         if (dataNamespace.isEmpty())
611             dataNamespace = "BinaryData";
612 
613         resourceFile.setClassName (dataNamespace);
614 
615         auto maxSize = project.getMaxBinaryFileSize();
616 
617         if (maxSize <= 0)
618             maxSize = 10 * 1024 * 1024;
619 
620         Array<File> binaryDataFiles;
621         auto r = resourceFile.write (maxSize);
622 
623         if (r.result.wasOk())
624         {
625             hasBinaryData = true;
626 
627             for (auto& f : r.filesCreated)
628             {
629                 filesCreated.add (f);
630                 generatedFilesGroup.addFileRetainingSortOrder (f, ! f.hasFileExtension (".h"));
631             }
632         }
633         else
634         {
635             addError (r.result.getErrorMessage());
636         }
637     }
638     else
639     {
640         for (int i = 20; --i >= 0;)
641             project.getBinaryDataCppFile (i).deleteFile();
642 
643         binaryDataH.deleteFile();
644     }
645 }
646 
writeReadmeFile()647 void ProjectSaver::writeReadmeFile()
648 {
649     MemoryOutputStream out;
650     out.setNewLineString (projectLineFeed);
651 
652     out << newLine
653         << " Important Note!!" << newLine
654         << " ================" << newLine
655         << newLine
656         << "The purpose of this folder is to contain files that are auto-generated by the Projucer," << newLine
657         << "and ALL files in this folder will be mercilessly DELETED and completely re-written whenever" << newLine
658         << "the Projucer saves your project." << newLine
659         << newLine
660         << "Therefore, it's a bad idea to make any manual changes to the files in here, or to" << newLine
661         << "put any of your own files in here if you don't want to lose them. (Of course you may choose" << newLine
662         << "to add the folder's contents to your version-control system so that you can re-merge your own" << newLine
663         << "modifications after the Projucer has saved its changes)." << newLine;
664 
665     replaceFileIfDifferent (generatedCodeFolder.getChildFile ("ReadMe.txt"), out);
666 }
667 
getAudioPluginDefines() const668 String ProjectSaver::getAudioPluginDefines() const
669 {
670     const auto flags = project.getAudioPluginFlags();
671 
672     if (flags.size() == 0)
673         return {};
674 
675     MemoryOutputStream mem;
676     mem.setNewLineString (projectLineFeed);
677 
678     mem << "//==============================================================================" << newLine
679         << "// Audio plugin settings.." << newLine
680         << newLine;
681 
682     for (int i = 0; i < flags.size(); ++i)
683     {
684         mem << "#ifndef  " << flags.getAllKeys()[i] << newLine
685             << " #define " << flags.getAllKeys()[i].paddedRight (' ', 32) << "  "
686                            << flags.getAllValues()[i] << newLine
687             << "#endif" << newLine;
688     }
689 
690     return mem.toString().trim();
691 }
692 
writeUnityScriptFile()693 void ProjectSaver::writeUnityScriptFile()
694 {
695     auto unityScriptContents = replaceLineFeeds (BinaryData::UnityPluginGUIScript_cs_in,
696                                                  projectLineFeed);
697 
698     auto projectName = Project::addUnityPluginPrefixIfNecessary (project.getProjectNameString());
699 
700     unityScriptContents = unityScriptContents.replace ("${plugin_class_name}",  projectName.replace (" ", "_"))
701                                              .replace ("${plugin_name}",        projectName)
702                                              .replace ("${plugin_vendor}",      project.getPluginManufacturerString())
703                                              .replace ("${plugin_description}", project.getPluginDescriptionString());
704 
705     auto f = generatedCodeFolder.getChildFile (project.getUnityScriptName());
706 
707     MemoryOutputStream out;
708     out << unityScriptContents;
709 
710     replaceFileIfDifferent (f, out);
711 }
712 
writeProjects(const OwnedArray<LibraryModule> & modules,ProjectExporter * specifiedExporterToSave)713 void ProjectSaver::writeProjects (const OwnedArray<LibraryModule>& modules, ProjectExporter* specifiedExporterToSave)
714 {
715     ThreadPool threadPool;
716 
717     // keep a copy of the basic generated files group, as each exporter may modify it.
718     auto originalGeneratedGroup = generatedFilesGroup.state.createCopy();
719 
720     CLionProjectExporter* clionExporter = nullptr;
721     std::vector<std::unique_ptr<ProjectExporter>> exporters;
722 
723     try
724     {
725         for (Project::ExporterIterator exp (project); exp.next();)
726         {
727             if (specifiedExporterToSave != nullptr && exp->getUniqueName() != specifiedExporterToSave->getUniqueName())
728                 continue;
729 
730             exporters.push_back (std::move (exp.exporter));
731         }
732 
733         for (auto& exporter : exporters)
734         {
735             exporter->initialiseDependencyPathValues();
736 
737             if (exporter->getTargetFolder().createDirectory())
738             {
739                 if (exporter->isCLion())
740                 {
741                     clionExporter = dynamic_cast<CLionProjectExporter*> (exporter.get());
742                 }
743                 else
744                 {
745                     exporter->copyMainGroupFromProject();
746                     exporter->settings = exporter->settings.createCopy();
747 
748                     exporter->addToExtraSearchPaths (build_tools::RelativePath ("JuceLibraryCode", build_tools::RelativePath::projectFolder));
749 
750                     generatedFilesGroup.state = originalGeneratedGroup.createCopy();
751                     exporter->addSettingsForProjectType (project.getProjectType());
752 
753                     for (auto* module : modules)
754                         module->addSettingsForModuleToExporter (*exporter, *this);
755 
756                     generatedFilesGroup.sortAlphabetically (true, true);
757                     exporter->getAllGroups().add (generatedFilesGroup);
758                 }
759 
760                 if (ProjucerApplication::getApp().isRunningCommandLine)
761                     saveExporter (*exporter, modules);
762                 else
763                     threadPool.addJob ([this, &exporter, &modules] { saveExporter (*exporter, modules); });
764             }
765             else
766             {
767                 addError ("Can't create folder: " + exporter->getTargetFolder().getFullPathName());
768             }
769         }
770     }
771     catch (build_tools::SaveError& saveError)
772     {
773         addError (saveError.message);
774     }
775 
776     while (threadPool.getNumJobs() > 0)
777         Thread::sleep (10);
778 
779     if (clionExporter != nullptr)
780     {
781         for (auto& exporter : exporters)
782             clionExporter->writeCMakeListsExporterSection (exporter.get());
783 
784         std::cout << "Finished saving: " << clionExporter->getUniqueName() << std::endl;
785     }
786 }
787 
runPostExportScript()788 void ProjectSaver::runPostExportScript()
789 {
790    #if JUCE_WINDOWS
791     auto cmdString = project.getPostExportShellCommandWinString();
792    #else
793     auto cmdString = project.getPostExportShellCommandPosixString();
794    #endif
795 
796     auto shellCommand = cmdString.replace ("%%1%%", project.getProjectFolder().getFullPathName());
797 
798     if (shellCommand.isNotEmpty())
799     {
800        #if JUCE_WINDOWS
801         StringArray argList ("cmd.exe", "/c");
802        #else
803         StringArray argList ("/bin/sh", "-c");
804        #endif
805 
806         argList.add (shellCommand);
807         ChildProcess shellProcess;
808 
809         if (! shellProcess.start (argList))
810         {
811             addError ("Failed to run shell command: " + argList.joinIntoString (" "));
812             return;
813         }
814 
815         if (! shellProcess.waitForProcessToFinish (10000))
816         {
817             addError ("Timeout running shell command: " + argList.joinIntoString (" "));
818             return;
819         }
820 
821         auto exitCode = shellProcess.getExitCode();
822 
823         if (exitCode != 0)
824             addError ("Shell command: " + argList.joinIntoString (" ") + " failed with exit code: " + String (exitCode));
825     }
826 }
827 
saveExporter(ProjectExporter & exporter,const OwnedArray<LibraryModule> & modules)828 void ProjectSaver::saveExporter (ProjectExporter& exporter, const OwnedArray<LibraryModule>& modules)
829 {
830     try
831     {
832         exporter.create (modules);
833 
834         if (! exporter.isCLion())
835         {
836             auto outputString = "Finished saving: " + exporter.getUniqueName();
837 
838             if (MessageManager::getInstance()->isThisTheMessageThread())
839                 std::cout <<  outputString << std::endl;
840             else
841                 MessageManager::callAsync ([outputString] { std::cout <<  outputString << std::endl; });
842         }
843     }
844     catch (build_tools::SaveError& error)
845     {
846         addError (error.message);
847     }
848 }
849