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