The JUCE cross-platform C++ framework, with DISTRHO/KXStudio specific changes
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

599 lines
20KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2017 - ROLI Ltd.
  5. JUCE is an open source library subject to commercial or open-source
  6. licensing.
  7. By using JUCE, you agree to the terms of both the JUCE 5 End-User License
  8. Agreement and JUCE 5 Privacy Policy (both updated and effective as of the
  9. 27th April 2017).
  10. End User License Agreement: www.juce.com/juce-5-licence
  11. Privacy Policy: www.juce.com/juce-5-privacy-policy
  12. Or: You may also use this code under the terms of the GPL v3 (see
  13. www.gnu.org/licenses).
  14. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  15. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  16. DISCLAIMED.
  17. ==============================================================================
  18. */
  19. #include "../../Application/jucer_Headers.h"
  20. #include "../../ProjectSaving/jucer_ProjectExporter.h"
  21. #include "jucer_PIPGenerator.h"
  22. //==============================================================================
  23. static String removeEnclosed (const String& input, const String& start, const String& end)
  24. {
  25. auto startIndex = input.indexOf (start);
  26. auto endIndex = input.indexOf (end) + end.length();
  27. if (startIndex != -1 && endIndex != -1)
  28. return input.replaceSection (startIndex, endIndex - startIndex, {});
  29. return input;
  30. }
  31. static void ensureSingleNewLineAfterIncludes (StringArray& lines)
  32. {
  33. int lastIncludeIndex = -1;
  34. for (int i = 0; i < lines.size(); ++i)
  35. {
  36. if (lines[i].contains ("#include"))
  37. lastIncludeIndex = i;
  38. }
  39. if (lastIncludeIndex != -1)
  40. {
  41. auto index = lastIncludeIndex;
  42. int numNewLines = 0;
  43. while (++index < lines.size() && lines[index].isEmpty())
  44. ++numNewLines;
  45. if (numNewLines > 1)
  46. lines.removeRange (lastIncludeIndex + 1, numNewLines - 1);
  47. }
  48. }
  49. static String ensureCorrectWhitespace (StringRef input)
  50. {
  51. auto lines = StringArray::fromLines (input);
  52. ensureSingleNewLineAfterIncludes (lines);
  53. return joinLinesIntoSourceFile (lines);
  54. }
  55. static bool isJUCEExample (const File& pipFile)
  56. {
  57. int numLinesToTest = 10; // license should be at the top of the file so no need to
  58. // check all lines
  59. for (auto line : StringArray::fromLines (pipFile.loadFileAsString()))
  60. {
  61. if (line.contains ("This file is part of the JUCE examples."))
  62. return true;
  63. --numLinesToTest;
  64. }
  65. return false;
  66. }
  67. static bool isValidExporterName (StringRef exporterName)
  68. {
  69. return ProjectExporter::getExporterValueTreeNames().contains (exporterName, true);
  70. }
  71. static bool isMobileExporter (const String& exporterName)
  72. {
  73. return exporterName == "XCODE_IPHONE" || exporterName == "ANDROIDSTUDIO";
  74. }
  75. //==============================================================================
  76. PIPGenerator::PIPGenerator (const File& pip, const File& output)
  77. : pipFile (pip),
  78. metadata (parsePIPMetadata())
  79. {
  80. if (output != File())
  81. {
  82. outputDirectory = output;
  83. isTemp = false;
  84. }
  85. else
  86. {
  87. outputDirectory = File::getSpecialLocation (File::SpecialLocationType::tempDirectory).getChildFile ("PIPs");
  88. isTemp = true;
  89. }
  90. auto isClipboard = (pip.getParentDirectory().getFileName() == "Clipboard"
  91. && pip.getParentDirectory().getParentDirectory().getFileName() == "PIPs");
  92. outputDirectory = outputDirectory.getChildFile (metadata[Ids::name].toString());
  93. useLocalCopy = metadata[Ids::useLocalCopy].toString().isNotEmpty() || isClipboard;
  94. }
  95. //==============================================================================
  96. Result PIPGenerator::createJucerFile()
  97. {
  98. ValueTree root (Ids::JUCERPROJECT);
  99. auto result = setProjectSettings (root);
  100. if (result != Result::ok())
  101. return result;
  102. addModules (root);
  103. addExporters (root);
  104. createFiles (root);
  105. if (! metadata[Ids::moduleFlags].toString().isEmpty())
  106. setModuleFlags (root);
  107. auto outputFile = outputDirectory.getChildFile (metadata[Ids::name].toString() + ".jucer");
  108. std::unique_ptr<XmlElement> xml (root.createXml());
  109. if (xml->writeToFile (outputFile, {}))
  110. return Result::ok();
  111. return Result::fail ("Failed to create .jucer file in " + outputDirectory.getFullPathName());
  112. }
  113. Result PIPGenerator::createMainCpp()
  114. {
  115. auto outputFile = outputDirectory.getChildFile ("Source").getChildFile ("Main.cpp");
  116. if (! outputFile.existsAsFile() && (outputFile.create() != Result::ok()))
  117. return Result::fail ("Failed to create Main.cpp - " + outputFile.getFullPathName());
  118. outputFile.replaceWithText (getMainFileTextForType());
  119. return Result::ok();
  120. }
  121. //==============================================================================
  122. var PIPGenerator::parsePIPMetadata (const StringArray& lines)
  123. {
  124. auto* o = new DynamicObject();
  125. var result (o);
  126. for (auto& line : lines)
  127. {
  128. line = trimCommentCharsFromStartOfLine (line);
  129. auto colon = line.indexOfChar (':');
  130. if (colon >= 0)
  131. {
  132. auto key = line.substring (0, colon).trim();
  133. auto value = line.substring (colon + 1).trim();
  134. o->setProperty (key, value);
  135. }
  136. }
  137. return result;
  138. }
  139. static String parseMetadataItem (const StringArray& lines, int& index)
  140. {
  141. String result = lines[index++];
  142. while (index < lines.size())
  143. {
  144. auto continuationLine = trimCommentCharsFromStartOfLine (lines[index]);
  145. if (continuationLine.indexOfChar (':') != -1 || continuationLine.startsWith ("END_JUCE_PIP_METADATA"))
  146. break;
  147. result += continuationLine;
  148. ++index;
  149. }
  150. return result;
  151. }
  152. var PIPGenerator::parsePIPMetadata()
  153. {
  154. StringArray lines;
  155. pipFile.readLines (lines);
  156. for (int i = 0; i < lines.size(); ++i)
  157. {
  158. auto trimmedLine = trimCommentCharsFromStartOfLine (lines[i]);
  159. if (trimmedLine.startsWith ("BEGIN_JUCE_PIP_METADATA"))
  160. {
  161. StringArray desc;
  162. auto j = i + 1;
  163. while (j < lines.size())
  164. {
  165. if (trimCommentCharsFromStartOfLine (lines[j]).startsWith ("END_JUCE_PIP_METADATA"))
  166. return parsePIPMetadata (desc);
  167. desc.add (parseMetadataItem (lines, j));
  168. }
  169. }
  170. }
  171. return {};
  172. }
  173. //==============================================================================
  174. void PIPGenerator::addFileToTree (ValueTree& groupTree, const String& name, bool compile, const String& path)
  175. {
  176. ValueTree file (Ids::FILE);
  177. file.setProperty (Ids::ID, createAlphaNumericUID(), nullptr);
  178. file.setProperty (Ids::name, name, nullptr);
  179. file.setProperty (Ids::compile, compile, nullptr);
  180. file.setProperty (Ids::resource, 0, nullptr);
  181. file.setProperty (Ids::file, path, nullptr);
  182. groupTree.addChild (file, -1, nullptr);
  183. }
  184. void PIPGenerator::createFiles (ValueTree& jucerTree)
  185. {
  186. auto sourceDir = outputDirectory.getChildFile ("Source");
  187. if (! sourceDir.exists())
  188. sourceDir.createDirectory();
  189. if (useLocalCopy)
  190. pipFile.copyFileTo (sourceDir.getChildFile (pipFile.getFileName()));
  191. ValueTree mainGroup (Ids::MAINGROUP);
  192. mainGroup.setProperty (Ids::ID, createAlphaNumericUID(), nullptr);
  193. mainGroup.setProperty (Ids::name, metadata[Ids::name], nullptr);
  194. ValueTree group (Ids::GROUP);
  195. group.setProperty (Ids::ID, createGUID (sourceDir.getFullPathName() + "_guidpathsaltxhsdf"), nullptr);
  196. group.setProperty (Ids::name, "Source", nullptr);
  197. addFileToTree (group, "Main.cpp", true, "Source/Main.cpp");
  198. addFileToTree (group, pipFile.getFileName(), false, useLocalCopy ? "Source/" + pipFile.getFileName()
  199. : pipFile.getFullPathName());
  200. mainGroup.addChild (group, -1, nullptr);
  201. if (useLocalCopy)
  202. {
  203. auto relativeFiles = replaceRelativeIncludesAndGetFilesToMove();
  204. if (relativeFiles.size() > 0)
  205. {
  206. ValueTree assets (Ids::GROUP);
  207. assets.setProperty (Ids::ID, createAlphaNumericUID(), nullptr);
  208. assets.setProperty (Ids::name, "Assets", nullptr);
  209. for (auto& f : relativeFiles)
  210. if (copyRelativeFileToLocalSourceDirectory (f))
  211. addFileToTree (assets, f.getFileName(), f.getFileExtension() == ".cpp", "Source/" + f.getFileName());
  212. mainGroup.addChild (assets, -1, nullptr);
  213. }
  214. }
  215. jucerTree.addChild (mainGroup, 0, nullptr);
  216. }
  217. ValueTree PIPGenerator::createModulePathChild (const String& moduleID)
  218. {
  219. ValueTree modulePath (Ids::MODULEPATH);
  220. modulePath.setProperty (Ids::ID, moduleID, nullptr);
  221. modulePath.setProperty (Ids::path, {}, nullptr);
  222. return modulePath;
  223. }
  224. ValueTree PIPGenerator::createBuildConfigChild (bool isDebug)
  225. {
  226. ValueTree child (Ids::CONFIGURATIONS);
  227. child.setProperty (Ids::name, isDebug ? "Debug" : "Release", nullptr);
  228. child.setProperty (Ids::isDebug, isDebug ? 1 : 0, nullptr);
  229. child.setProperty (Ids::optimisation, isDebug ? 1 : 3, nullptr);
  230. child.setProperty (Ids::targetName, metadata[Ids::name], nullptr);
  231. return child;
  232. }
  233. ValueTree PIPGenerator::createExporterChild (const String& exporterName)
  234. {
  235. ValueTree exporter (exporterName);
  236. exporter.setProperty (Ids::targetFolder, "Builds/" + ProjectExporter::getTargetFolderForExporter (exporterName), nullptr);
  237. if (isMobileExporter (exporterName) || (metadata[Ids::name] == "AUv3SynthPlugin" && exporterName == "XCODE_MAC"))
  238. {
  239. auto juceDir = getAppSettings().getStoredPath (Ids::jucePath).toString();
  240. if (juceDir.isNotEmpty() && isValidJUCEExamplesDirectory (File (juceDir).getChildFile ("examples")))
  241. {
  242. auto assetsDirectoryPath = File (juceDir).getChildFile ("examples").getChildFile ("Assets").getFullPathName();
  243. exporter.setProperty (exporterName == "ANDROIDSTUDIO" ? Ids::androidExtraAssetsFolder
  244. : Ids::customXcodeResourceFolders,
  245. assetsDirectoryPath, nullptr);
  246. }
  247. else
  248. {
  249. // invalid JUCE path
  250. jassertfalse;
  251. }
  252. }
  253. {
  254. ValueTree configs (Ids::CONFIGURATIONS);
  255. configs.addChild (createBuildConfigChild (true), -1, nullptr);
  256. configs.addChild (createBuildConfigChild (false), -1, nullptr);
  257. exporter.addChild (configs, -1, nullptr);
  258. }
  259. {
  260. ValueTree modulePaths (Ids::MODULEPATHS);
  261. auto modules = StringArray::fromTokens (metadata[Ids::dependencies_].toString(), ",", {});
  262. for (auto m : modules)
  263. {
  264. m = m.trim();
  265. if (isJUCEModule (m))
  266. modulePaths.addChild (createModulePathChild (m), -1, nullptr);
  267. }
  268. exporter.addChild (modulePaths, -1, nullptr);
  269. }
  270. return exporter;
  271. }
  272. ValueTree PIPGenerator::createModuleChild (const String& moduleID)
  273. {
  274. ValueTree module (Ids::MODULE);
  275. module.setProperty (Ids::ID, moduleID, nullptr);
  276. module.setProperty (Ids::showAllCode, 1, nullptr);
  277. module.setProperty (Ids::useLocalCopy, 0, nullptr);
  278. module.setProperty (Ids::useGlobalPath, 1, nullptr);
  279. return module;
  280. }
  281. void PIPGenerator::addExporters (ValueTree& jucerTree)
  282. {
  283. ValueTree exportersTree (Ids::EXPORTFORMATS);
  284. auto exporters = StringArray::fromTokens (metadata[Ids::exporters].toString(), ",", {});
  285. for (auto& e : exporters)
  286. {
  287. e = e.trim().toUpperCase();
  288. if (isValidExporterName (e))
  289. exportersTree.addChild (createExporterChild (e), -1, nullptr);
  290. }
  291. jucerTree.addChild (exportersTree, -1, nullptr);
  292. }
  293. void PIPGenerator::addModules (ValueTree& jucerTree)
  294. {
  295. ValueTree modulesTree (Ids::MODULES);
  296. auto modules = StringArray::fromTokens (metadata[Ids::dependencies_].toString(), ",", {});
  297. modules.trim();
  298. auto projectType = metadata[Ids::type].toString();
  299. if (projectType == "Console")
  300. modules.mergeArray (getModulesRequiredForConsole());
  301. else if (projectType == "Component")
  302. modules.mergeArray (getModulesRequiredForComponent());
  303. else if (projectType == "AudioProcessor")
  304. modules.mergeArray (getModulesRequiredForAudioProcessor());
  305. for (auto& m : modules)
  306. {
  307. m = m.trim();
  308. if (isJUCEModule (m))
  309. modulesTree.addChild (createModuleChild (m), -1, nullptr);
  310. }
  311. jucerTree.addChild (modulesTree, -1, nullptr);
  312. }
  313. Result PIPGenerator::setProjectSettings (ValueTree& jucerTree)
  314. {
  315. jucerTree.setProperty (Ids::name, metadata[Ids::name], nullptr);
  316. jucerTree.setProperty (Ids::companyName, metadata[Ids::vendor], nullptr);
  317. jucerTree.setProperty (Ids::version, metadata[Ids::version], nullptr);
  318. jucerTree.setProperty (Ids::userNotes, metadata[Ids::description], nullptr);
  319. jucerTree.setProperty (Ids::companyWebsite, metadata[Ids::website], nullptr);
  320. auto defines = metadata[Ids::defines].toString();
  321. if (useLocalCopy && isJUCEExample (pipFile))
  322. {
  323. auto juceDir = getAppSettings().getStoredPath (Ids::jucePath).toString();
  324. if (juceDir.isNotEmpty() && isValidJUCEExamplesDirectory (File (juceDir).getChildFile ("examples")))
  325. {
  326. defines += ((defines.isEmpty() ? "" : " ") + String ("PIP_JUCE_EXAMPLES_DIRECTORY=")
  327. + Base64::toBase64 (File (juceDir).getChildFile ("examples").getFullPathName()));
  328. }
  329. else
  330. {
  331. return Result::fail (String ("Invalid JUCE path. Set path to JUCE via ") +
  332. (TargetOS::getThisOS() == TargetOS::osx ? "\"Projucer->Global Paths...\""
  333. : "\"File->Global Paths...\"")
  334. + " menu item.");
  335. }
  336. }
  337. jucerTree.setProperty (Ids::defines, defines, nullptr);
  338. auto type = metadata[Ids::type].toString();
  339. if (type == "Console")
  340. {
  341. jucerTree.setProperty (Ids::projectType, "consoleapp", nullptr);
  342. }
  343. else if (type == "Component")
  344. {
  345. jucerTree.setProperty (Ids::projectType, "guiapp", nullptr);
  346. }
  347. else if (type == "AudioProcessor")
  348. {
  349. jucerTree.setProperty (Ids::projectType, "audioplug", nullptr);
  350. jucerTree.setProperty (Ids::pluginManufacturer, metadata[Ids::vendor], nullptr);
  351. jucerTree.setProperty (Ids::pluginAUIsSandboxSafe, "1", nullptr);
  352. StringArray pluginFormatsToBuild (Ids::buildVST3.toString(), Ids::buildAU.toString(), Ids::buildStandalone.toString());
  353. pluginFormatsToBuild.addArray (getExtraPluginFormatsToBuild());
  354. jucerTree.setProperty (Ids::pluginFormats, pluginFormatsToBuild.joinIntoString (","), nullptr);
  355. if (! getPluginCharacteristics().isEmpty())
  356. jucerTree.setProperty (Ids::pluginCharacteristicsValue, getPluginCharacteristics().joinIntoString (","), nullptr);
  357. }
  358. return Result::ok();
  359. }
  360. void PIPGenerator::setModuleFlags (ValueTree& jucerTree)
  361. {
  362. ValueTree options ("JUCEOPTIONS");
  363. for (auto& option : StringArray::fromTokens (metadata[Ids::moduleFlags].toString(), ",", {}))
  364. {
  365. auto name = option.upToFirstOccurrenceOf ("=", false, true).trim();
  366. auto value = option.fromFirstOccurrenceOf ("=", false, true).trim();
  367. options.setProperty (name, (value == "1" ? 1 : 0), nullptr);
  368. }
  369. jucerTree.addChild (options, -1, nullptr);
  370. }
  371. String PIPGenerator::getMainFileTextForType()
  372. {
  373. String mainTemplate (BinaryData::jucer_PIPMain_cpp);
  374. mainTemplate = mainTemplate.replace ("%%filename%%", useLocalCopy ? pipFile.getFileName()
  375. : pipFile.getFullPathName());
  376. auto type = metadata[Ids::type].toString();
  377. if (type == "Console")
  378. {
  379. mainTemplate = removeEnclosed (mainTemplate, "%%component_begin%%", "%%component_end%%");
  380. mainTemplate = removeEnclosed (mainTemplate, "%%audioprocessor_begin%%", "%%audioprocessor_end%%");
  381. mainTemplate = mainTemplate.replace ("%%console_begin%%", {}).replace ("%%console_end%%", {});
  382. return ensureCorrectWhitespace (mainTemplate);
  383. }
  384. else if (type == "Component")
  385. {
  386. mainTemplate = removeEnclosed (mainTemplate, "%%audioprocessor_begin%%", "%%audioprocessor_end%%");
  387. mainTemplate = removeEnclosed (mainTemplate, "%%console_begin%%", "%%console_end%%");
  388. mainTemplate = mainTemplate.replace ("%%component_begin%%", {}).replace ("%%component_end%%", {});
  389. mainTemplate = mainTemplate.replace ("%%project_name%%", metadata[Ids::name].toString());
  390. mainTemplate = mainTemplate.replace ("%%project_version%%", metadata[Ids::version].toString());
  391. return ensureCorrectWhitespace (mainTemplate.replace ("%%startup%%", "mainWindow.reset (new MainWindow (" + metadata[Ids::name].toString().quoted()
  392. + ", new " + metadata[Ids::mainClass].toString() + "(), *this));")
  393. .replace ("%%shutdown%%", "mainWindow = nullptr;"));
  394. }
  395. else if (type == "AudioProcessor")
  396. {
  397. mainTemplate = removeEnclosed (mainTemplate, "%%component_begin%%", "%%component_end%%");
  398. mainTemplate = removeEnclosed (mainTemplate, "%%console_begin%%", "%%console_end%%");
  399. mainTemplate = mainTemplate.replace ("%%audioprocessor_begin%%", {}).replace ("%%audioprocessor_end%%", {});
  400. return ensureCorrectWhitespace (mainTemplate.replace ("%%class_name%%", metadata[Ids::mainClass].toString()));
  401. }
  402. return {};
  403. }
  404. //==============================================================================
  405. Array<File> PIPGenerator::replaceRelativeIncludesAndGetFilesToMove()
  406. {
  407. StringArray lines;
  408. pipFile.readLines (lines);
  409. Array<File> files;
  410. for (auto& line : lines)
  411. {
  412. if (line.contains ("#include") && ! line.contains ("JuceLibraryCode"))
  413. {
  414. auto path = line.fromFirstOccurrenceOf ("#include", false, false);
  415. path = path.removeCharacters ("\"").trim();
  416. if (path.startsWith ("<") && path.endsWith (">"))
  417. continue;
  418. auto file = pipFile.getParentDirectory().getChildFile (path);
  419. files.add (file);
  420. line = line.replace (path, file.getFileName());
  421. }
  422. }
  423. outputDirectory.getChildFile ("Source")
  424. .getChildFile (pipFile.getFileName())
  425. .replaceWithText (joinLinesIntoSourceFile (lines));
  426. return files;
  427. }
  428. bool PIPGenerator::copyRelativeFileToLocalSourceDirectory (const File& fileToCopy) const noexcept
  429. {
  430. return fileToCopy.copyFileTo (outputDirectory.getChildFile ("Source")
  431. .getChildFile (fileToCopy.getFileName()));
  432. }
  433. StringArray PIPGenerator::getExtraPluginFormatsToBuild() const
  434. {
  435. auto name = metadata[Ids::name].toString();
  436. if (name == "AUv3SynthPlugin" || name == "AudioPluginDemo")
  437. return { Ids::buildAUv3.toString() };
  438. else if (name == "InterAppAudioEffectPlugin")
  439. return { Ids::enableIAA.toString() };
  440. return {};
  441. }
  442. StringArray PIPGenerator::getPluginCharacteristics() const
  443. {
  444. auto name = metadata[Ids::name].toString();
  445. if (name == "AudioPluginDemo")
  446. return { Ids::pluginWantsMidiIn.toString(),
  447. Ids::pluginProducesMidiOut.toString(),
  448. Ids::pluginEditorRequiresKeys.toString() };
  449. else if (name == "AUv3SynthPlugin" || name == "MultiOutSynthPlugin")
  450. return { Ids::pluginWantsMidiIn.toString(),
  451. Ids::pluginIsSynth.toString() };
  452. else if (name == "ArpeggiatorPlugin")
  453. return { Ids::pluginIsMidiEffectPlugin.toString() };
  454. return {};
  455. }