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 "../../Application/jucer_Headers.h"
27 #include "../../ProjectSaving/jucer_ProjectExporter.h"
28 #include "../../ProjectSaving/jucer_ProjectExport_Xcode.h"
29 #include "../../ProjectSaving/jucer_ProjectExport_Android.h"
30 #include "jucer_PIPGenerator.h"
31 
32 //==============================================================================
ensureSingleNewLineAfterIncludes(StringArray & lines)33 static void ensureSingleNewLineAfterIncludes (StringArray& lines)
34 {
35     int lastIncludeIndex = -1;
36 
37     for (int i = 0; i < lines.size(); ++i)
38     {
39         if (lines[i].contains ("#include"))
40             lastIncludeIndex = i;
41     }
42 
43     if (lastIncludeIndex != -1)
44     {
45         auto index = lastIncludeIndex;
46         int numNewLines = 0;
47 
48         while (++index < lines.size() && lines[index].isEmpty())
49             ++numNewLines;
50 
51         if (numNewLines > 1)
52             lines.removeRange (lastIncludeIndex + 1, numNewLines - 1);
53     }
54 }
55 
ensureCorrectWhitespace(StringRef input)56 static String ensureCorrectWhitespace (StringRef input)
57 {
58     auto lines = StringArray::fromLines (input);
59     ensureSingleNewLineAfterIncludes (lines);
60     return joinLinesIntoSourceFile (lines);
61 }
62 
isJUCEExample(const File & pipFile)63 static bool isJUCEExample (const File& pipFile)
64 {
65     int numLinesToTest = 10; // license should be at the top of the file so no need to
66                              // check all lines
67 
68     for (auto line : StringArray::fromLines (pipFile.loadFileAsString()))
69     {
70         if (line.contains ("This file is part of the JUCE examples."))
71             return true;
72 
73         --numLinesToTest;
74     }
75 
76     return false;
77 }
78 
isValidExporterIdentifier(const Identifier & exporterIdentifier)79 static bool isValidExporterIdentifier (const Identifier& exporterIdentifier)
80 {
81     return ProjectExporter::getTypeInfoForExporter (exporterIdentifier).identifier.toString().isNotEmpty();
82 }
83 
exporterRequiresExampleAssets(const Identifier & exporterIdentifier,const String & projectName)84 static bool exporterRequiresExampleAssets (const Identifier& exporterIdentifier, const String& projectName)
85 {
86     return (exporterIdentifier.toString() == XcodeProjectExporter::getValueTreeTypeNameiOS()
87             || exporterIdentifier.toString() == AndroidProjectExporter::getValueTreeTypeName())
88             || (exporterIdentifier.toString() == XcodeProjectExporter::getValueTreeTypeNameMac() && projectName == "AUv3SynthPlugin");
89 }
90 
91 //==============================================================================
PIPGenerator(const File & pip,const File & output,const File & jucePath,const File & userPath)92 PIPGenerator::PIPGenerator (const File& pip, const File& output, const File& jucePath, const File& userPath)
93     : pipFile (pip),
94       juceModulesPath (jucePath),
95       userModulesPath (userPath),
96       metadata (parseJUCEHeaderMetadata (pipFile))
97 {
98     if (output != File())
99     {
100         outputDirectory = output;
101         isTemp = false;
102     }
103     else
104     {
105         outputDirectory = File::getSpecialLocation (File::SpecialLocationType::tempDirectory).getChildFile ("PIPs");
106         isTemp = true;
107     }
108 
109     auto isClipboard = (pip.getParentDirectory().getFileName() == "Clipboard"
110                         && pip.getParentDirectory().getParentDirectory().getFileName() == "PIPs");
111 
112     outputDirectory = outputDirectory.getChildFile (metadata[Ids::name].toString()).getNonexistentSibling();
113     useLocalCopy = metadata[Ids::useLocalCopy].toString().trim().getIntValue() == 1 || isClipboard;
114 
115     if (userModulesPath != File())
116     {
117         availableUserModules.reset (new AvailableModulesList());
118         availableUserModules->scanPaths ({ userModulesPath });
119     }
120 }
121 
122 //==============================================================================
createJucerFile()123 Result PIPGenerator::createJucerFile()
124 {
125     ValueTree root (Ids::JUCERPROJECT);
126 
127     auto result = setProjectSettings (root);
128 
129     if (result != Result::ok())
130         return result;
131 
132     addModules     (root);
133     addExporters   (root);
134     createFiles    (root);
135     setModuleFlags (root);
136 
137     auto outputFile = outputDirectory.getChildFile (metadata[Ids::name].toString() + ".jucer");
138 
139     if (auto xml = root.createXml())
140         if (xml->writeTo (outputFile, {}))
141             return Result::ok();
142 
143     return Result::fail ("Failed to create .jucer file in " + outputDirectory.getFullPathName());
144 }
145 
createMainCpp()146 Result PIPGenerator::createMainCpp()
147 {
148     auto outputFile = outputDirectory.getChildFile ("Source").getChildFile ("Main.cpp");
149 
150     if (! outputFile.existsAsFile() && (outputFile.create() != Result::ok()))
151         return Result::fail ("Failed to create Main.cpp - " + outputFile.getFullPathName());
152 
153     outputFile.replaceWithText (getMainFileTextForType());
154 
155     return Result::ok();
156 }
157 
158 //==============================================================================
addFileToTree(ValueTree & groupTree,const String & name,bool compile,const String & path)159 void PIPGenerator::addFileToTree (ValueTree& groupTree, const String& name, bool compile, const String& path)
160 {
161     ValueTree file (Ids::FILE);
162     file.setProperty (Ids::ID, createAlphaNumericUID(), nullptr);
163     file.setProperty (Ids::name, name, nullptr);
164     file.setProperty (Ids::compile, compile, nullptr);
165     file.setProperty (Ids::resource, 0, nullptr);
166     file.setProperty (Ids::file, path, nullptr);
167 
168     groupTree.addChild (file, -1, nullptr);
169 }
170 
createFiles(ValueTree & jucerTree)171 void PIPGenerator::createFiles (ValueTree& jucerTree)
172 {
173     auto sourceDir = outputDirectory.getChildFile ("Source");
174 
175     if (! sourceDir.exists())
176         sourceDir.createDirectory();
177 
178     if (useLocalCopy)
179         pipFile.copyFileTo (sourceDir.getChildFile (pipFile.getFileName()));
180 
181     ValueTree mainGroup (Ids::MAINGROUP);
182     mainGroup.setProperty (Ids::ID, createAlphaNumericUID(), nullptr);
183     mainGroup.setProperty (Ids::name, metadata[Ids::name], nullptr);
184 
185     ValueTree group (Ids::GROUP);
186     group.setProperty (Ids::ID, createGUID (sourceDir.getFullPathName() + "_guidpathsaltxhsdf"), nullptr);
187     group.setProperty (Ids::name, "Source", nullptr);
188 
189     addFileToTree (group, "Main.cpp", true, "Source/Main.cpp");
190     addFileToTree (group, pipFile.getFileName(), false, useLocalCopy ? "Source/" + pipFile.getFileName()
191                                                                      : pipFile.getFullPathName());
192 
193     mainGroup.addChild (group, -1, nullptr);
194 
195     if (useLocalCopy)
196     {
197         auto relativeFiles = replaceRelativeIncludesAndGetFilesToMove();
198 
199         if (relativeFiles.size() > 0)
200         {
201             ValueTree assets (Ids::GROUP);
202             assets.setProperty (Ids::ID, createAlphaNumericUID(), nullptr);
203             assets.setProperty (Ids::name, "Assets", nullptr);
204 
205             for (auto& f : relativeFiles)
206                 if (copyRelativeFileToLocalSourceDirectory (f))
207                     addFileToTree (assets, f.getFileName(), f.getFileExtension() == ".cpp", "Source/" + f.getFileName());
208 
209             mainGroup.addChild (assets, -1, nullptr);
210         }
211     }
212 
213     jucerTree.addChild (mainGroup, 0, nullptr);
214 }
215 
createModulePathChild(const String & moduleID)216 ValueTree PIPGenerator::createModulePathChild (const String& moduleID)
217 {
218     ValueTree modulePath (Ids::MODULEPATH);
219 
220     modulePath.setProperty (Ids::ID, moduleID, nullptr);
221     modulePath.setProperty (Ids::path, getPathForModule (moduleID), nullptr);
222 
223     return modulePath;
224 }
225 
createBuildConfigChild(bool isDebug)226 ValueTree PIPGenerator::createBuildConfigChild (bool isDebug)
227 {
228     ValueTree child (Ids::CONFIGURATION);
229 
230     child.setProperty (Ids::name, isDebug ? "Debug" : "Release", nullptr);
231     child.setProperty (Ids::isDebug, isDebug ? 1 : 0, nullptr);
232     child.setProperty (Ids::optimisation, isDebug ? 1 : 3, nullptr);
233     child.setProperty (Ids::targetName, metadata[Ids::name], nullptr);
234 
235     return child;
236 }
237 
createExporterChild(const Identifier & exporterIdentifier)238 ValueTree PIPGenerator::createExporterChild (const Identifier& exporterIdentifier)
239 {
240     ValueTree exporter (exporterIdentifier);
241 
242     exporter.setProperty (Ids::targetFolder, "Builds/" + ProjectExporter::getTypeInfoForExporter (exporterIdentifier).targetFolder, nullptr);
243 
244     if (isJUCEExample (pipFile) && exporterRequiresExampleAssets (exporterIdentifier, metadata[Ids::name]))
245     {
246         auto examplesDir = getExamplesDirectory();
247 
248         if (examplesDir != File())
249         {
250             auto assetsDirectoryPath = examplesDir.getChildFile ("Assets").getFullPathName();
251 
252             exporter.setProperty (exporterIdentifier.toString() == AndroidProjectExporter::getValueTreeTypeName() ? Ids::androidExtraAssetsFolder
253                                                                                                                   : Ids::customXcodeResourceFolders,
254                                   assetsDirectoryPath, nullptr);
255         }
256         else
257         {
258             // invalid JUCE path
259             jassertfalse;
260         }
261     }
262 
263     {
264         ValueTree configs (Ids::CONFIGURATIONS);
265 
266         configs.addChild (createBuildConfigChild (true), -1, nullptr);
267         configs.addChild (createBuildConfigChild (false), -1, nullptr);
268 
269         exporter.addChild (configs, -1, nullptr);
270     }
271 
272     {
273         ValueTree modulePaths (Ids::MODULEPATHS);
274 
275         auto modules = StringArray::fromTokens (metadata[Ids::dependencies_].toString(), ",", {});
276 
277         for (auto m : modules)
278             modulePaths.addChild (createModulePathChild (m.trim()), -1, nullptr);
279 
280         exporter.addChild (modulePaths, -1, nullptr);
281     }
282 
283     return exporter;
284 }
285 
createModuleChild(const String & moduleID)286 ValueTree PIPGenerator::createModuleChild (const String& moduleID)
287 {
288     ValueTree module (Ids::MODULE);
289 
290     module.setProperty (Ids::ID, moduleID, nullptr);
291     module.setProperty (Ids::showAllCode, 1, nullptr);
292     module.setProperty (Ids::useLocalCopy, 0, nullptr);
293     module.setProperty (Ids::useGlobalPath, (getPathForModule (moduleID).isEmpty() ? 1 : 0), nullptr);
294 
295     return module;
296 }
297 
addExporters(ValueTree & jucerTree)298 void PIPGenerator::addExporters (ValueTree& jucerTree)
299 {
300     ValueTree exportersTree (Ids::EXPORTFORMATS);
301 
302     auto exporters = StringArray::fromTokens (metadata[Ids::exporters].toString(), ",", {});
303 
304     for (auto& e : exporters)
305     {
306         e = e.trim().toUpperCase();
307 
308         if (isValidExporterIdentifier (e))
309             exportersTree.addChild (createExporterChild (e), -1, nullptr);
310     }
311 
312     jucerTree.addChild (exportersTree, -1, nullptr);
313 }
314 
addModules(ValueTree & jucerTree)315 void PIPGenerator::addModules (ValueTree& jucerTree)
316 {
317     ValueTree modulesTree (Ids::MODULES);
318 
319     auto modules = StringArray::fromTokens (metadata[Ids::dependencies_].toString(), ",", {});
320     modules.trim();
321 
322     for (auto& m : modules)
323         modulesTree.addChild (createModuleChild (m.trim()), -1, nullptr);
324 
325     jucerTree.addChild (modulesTree, -1, nullptr);
326 }
327 
setProjectSettings(ValueTree & jucerTree)328 Result PIPGenerator::setProjectSettings (ValueTree& jucerTree)
329 {
330     auto setPropertyIfNotEmpty = [&jucerTree] (const Identifier& name, const var& value)
331     {
332         if (value != var())
333             jucerTree.setProperty (name, value, nullptr);
334     };
335 
336     setPropertyIfNotEmpty (Ids::name, metadata[Ids::name]);
337     setPropertyIfNotEmpty (Ids::companyName, metadata[Ids::vendor]);
338     setPropertyIfNotEmpty (Ids::version, metadata[Ids::version]);
339     setPropertyIfNotEmpty (Ids::userNotes, metadata[Ids::description]);
340     setPropertyIfNotEmpty (Ids::companyWebsite, metadata[Ids::website]);
341 
342     auto defines = metadata[Ids::defines].toString();
343 
344     if (isJUCEExample (pipFile))
345     {
346         auto examplesDir = getExamplesDirectory();
347 
348         if (examplesDir != File())
349         {
350              defines += ((defines.isEmpty() ? "" : " ") + String ("PIP_JUCE_EXAMPLES_DIRECTORY=")
351                          + Base64::toBase64 (examplesDir.getFullPathName()));
352         }
353         else
354         {
355             return Result::fail (String ("Invalid JUCE path. Set path to JUCE via ") +
356                                  (TargetOS::getThisOS() == TargetOS::osx ? "\"Projucer->Global Paths...\""
357                                                                          : "\"File->Global Paths...\"")
358                                  + " menu item.");
359         }
360 
361         jucerTree.setProperty (Ids::displaySplashScreen, true, nullptr);
362     }
363 
364     setPropertyIfNotEmpty (Ids::defines, defines);
365 
366     auto type = metadata[Ids::type].toString();
367 
368     if (type == "Console")
369     {
370         jucerTree.setProperty (Ids::projectType, build_tools::ProjectType_ConsoleApp::getTypeName(), nullptr);
371     }
372     else if (type == "Component")
373     {
374         jucerTree.setProperty (Ids::projectType, build_tools::ProjectType_GUIApp::getTypeName(), nullptr);
375     }
376     else if (type == "AudioProcessor")
377     {
378         jucerTree.setProperty (Ids::projectType, build_tools::ProjectType_AudioPlugin::getTypeName(), nullptr);
379         jucerTree.setProperty (Ids::pluginAUIsSandboxSafe, "1", nullptr);
380 
381         setPropertyIfNotEmpty (Ids::pluginManufacturer, metadata[Ids::vendor]);
382 
383         StringArray pluginFormatsToBuild (Ids::buildVST3.toString(), Ids::buildAU.toString(), Ids::buildStandalone.toString());
384         pluginFormatsToBuild.addArray (getExtraPluginFormatsToBuild());
385 
386         jucerTree.setProperty (Ids::pluginFormats, pluginFormatsToBuild.joinIntoString (","), nullptr);
387 
388         const auto characteristics = metadata[Ids::pluginCharacteristics].toString();
389 
390         if (characteristics.isNotEmpty())
391             jucerTree.setProperty (Ids::pluginCharacteristicsValue,
392                                    characteristics.removeCharacters (" \t\n\r"),
393                                    nullptr);
394     }
395 
396     jucerTree.setProperty (Ids::useAppConfig, false, nullptr);
397     jucerTree.setProperty (Ids::addUsingNamespaceToJuceHeader, true, nullptr);
398 
399     return Result::ok();
400 }
401 
setModuleFlags(ValueTree & jucerTree)402 void PIPGenerator::setModuleFlags (ValueTree& jucerTree)
403 {
404     ValueTree options ("JUCEOPTIONS");
405 
406     for (auto& option : StringArray::fromTokens (metadata[Ids::moduleFlags].toString(), ",", {}))
407     {
408         auto name  = option.upToFirstOccurrenceOf ("=", false, true).trim();
409         auto value = option.fromFirstOccurrenceOf ("=", false, true).trim();
410 
411         options.setProperty (name, (value == "1" ? 1 : 0), nullptr);
412     }
413 
414     if (metadata[Ids::type].toString() == "AudioProcessor"
415           && options.getPropertyPointer ("JUCE_VST3_CAN_REPLACE_VST2") == nullptr)
416         options.setProperty ("JUCE_VST3_CAN_REPLACE_VST2", 0, nullptr);
417 
418     jucerTree.addChild (options, -1, nullptr);
419 }
420 
getMainFileTextForType()421 String PIPGenerator::getMainFileTextForType()
422 {
423     const auto type = metadata[Ids::type].toString();
424 
425     const auto mainTemplate = [&]
426     {
427         if (type == "Console")
428             return String (BinaryData::PIPConsole_cpp_in);
429 
430         if (type == "Component")
431             return String (BinaryData::PIPComponent_cpp_in)
432                    .replace ("${JUCE_PIP_NAME}",       metadata[Ids::name].toString())
433                    .replace ("${PROJECT_VERSION}",     metadata[Ids::version].toString())
434                    .replace ("${JUCE_PIP_MAIN_CLASS}", metadata[Ids::mainClass].toString());
435 
436         if (type == "AudioProcessor")
437             return String (BinaryData::PIPAudioProcessor_cpp_in)
438                    .replace ("${JUCE_PIP_MAIN_CLASS}", metadata[Ids::mainClass].toString());
439 
440         return String{};
441     }();
442 
443     if (mainTemplate.isEmpty())
444         return {};
445 
446     const auto includeFilename = [&]
447     {
448         if (useLocalCopy) return pipFile.getFileName();
449         if (isTemp)       return pipFile.getFullPathName();
450 
451         return build_tools::RelativePath (pipFile,
452                                           outputDirectory.getChildFile ("Source"),
453                                           build_tools::RelativePath::unknown).toUnixStyle();
454     }();
455 
456     return ensureCorrectWhitespace (mainTemplate.replace ("${JUCE_PIP_HEADER}", includeFilename));
457 }
458 
459 //==============================================================================
replaceRelativeIncludesAndGetFilesToMove()460 Array<File> PIPGenerator::replaceRelativeIncludesAndGetFilesToMove()
461 {
462     StringArray lines;
463     pipFile.readLines (lines);
464     Array<File> files;
465 
466     for (auto& line : lines)
467     {
468         if (line.contains ("#include") && ! line.contains ("JuceLibraryCode"))
469         {
470             auto path = line.fromFirstOccurrenceOf ("#include", false, false);
471             path = path.removeCharacters ("\"").trim();
472 
473             if (path.startsWith ("<") && path.endsWith (">"))
474                 continue;
475 
476             auto file = pipFile.getParentDirectory().getChildFile (path);
477             files.add (file);
478 
479             line = line.replace (path, file.getFileName());
480         }
481     }
482 
483     outputDirectory.getChildFile ("Source")
484                    .getChildFile (pipFile.getFileName())
485                    .replaceWithText (joinLinesIntoSourceFile (lines));
486 
487     return files;
488 }
489 
copyRelativeFileToLocalSourceDirectory(const File & fileToCopy) const490 bool PIPGenerator::copyRelativeFileToLocalSourceDirectory (const File& fileToCopy) const noexcept
491 {
492     return fileToCopy.copyFileTo (outputDirectory.getChildFile ("Source")
493                                                  .getChildFile (fileToCopy.getFileName()));
494 }
495 
getExtraPluginFormatsToBuild() const496 StringArray PIPGenerator::getExtraPluginFormatsToBuild() const
497 {
498     auto tokens = StringArray::fromTokens (metadata[Ids::extraPluginFormats].toString(), ",", {});
499 
500     for (auto& token : tokens)
501     {
502         token = [&]
503         {
504             if (token == "IAA")
505                 return Ids::enableIAA.toString();
506 
507             return "build" + token;
508         }();
509     }
510 
511     return tokens;
512 }
513 
getPathForModule(const String & moduleID) const514 String PIPGenerator::getPathForModule (const String& moduleID) const
515 {
516     if (isJUCEModule (moduleID))
517     {
518         if (juceModulesPath != File())
519         {
520             if (isTemp)
521                 return juceModulesPath.getFullPathName();
522 
523             return build_tools::RelativePath (juceModulesPath,
524                                               outputDirectory,
525                                               build_tools::RelativePath::projectFolder).toUnixStyle();
526         }
527     }
528     else if (availableUserModules != nullptr)
529     {
530         auto moduleRoot = availableUserModules->getModuleWithID (moduleID).second.getParentDirectory();
531 
532         if (isTemp)
533             return moduleRoot.getFullPathName();
534 
535         return build_tools::RelativePath (moduleRoot,
536                                           outputDirectory,
537                                           build_tools::RelativePath::projectFolder).toUnixStyle();
538     }
539 
540     return {};
541 }
542 
getExamplesDirectory() const543 File PIPGenerator::getExamplesDirectory() const
544 {
545     if (juceModulesPath != File())
546     {
547         auto examples = juceModulesPath.getSiblingFile ("examples");
548 
549         if (isValidJUCEExamplesDirectory (examples))
550             return examples;
551     }
552 
553     auto examples = File (getAppSettings().getStoredPath (Ids::jucePath, TargetOS::getThisOS()).get().toString()).getChildFile ("examples");
554 
555     if (isValidJUCEExamplesDirectory (examples))
556         return examples;
557 
558     return {};
559 }
560