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.

578 lines
20KB

  1. /*
  2. ==============================================================================
  3. This file is part of the JUCE library.
  4. Copyright (c) 2022 - Raw Material Software Limited
  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 7 End-User License
  8. Agreement and JUCE Privacy Policy.
  9. End User License Agreement: www.juce.com/juce-7-licence
  10. Privacy Policy: www.juce.com/juce-privacy-policy
  11. Or: You may also use this code under the terms of the GPL v3 (see
  12. www.gnu.org/licenses).
  13. JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER
  14. EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE
  15. DISCLAIMED.
  16. ==============================================================================
  17. */
  18. #include "../../Application/jucer_Headers.h"
  19. #include "../../ProjectSaving/jucer_ProjectExporter.h"
  20. #include "../../ProjectSaving/jucer_ProjectExport_Xcode.h"
  21. #include "../../ProjectSaving/jucer_ProjectExport_Android.h"
  22. #include "jucer_PIPGenerator.h"
  23. //==============================================================================
  24. static void ensureSingleNewLineAfterIncludes (StringArray& lines)
  25. {
  26. int lastIncludeIndex = -1;
  27. for (int i = 0; i < lines.size(); ++i)
  28. {
  29. if (lines[i].contains ("#include"))
  30. lastIncludeIndex = i;
  31. }
  32. if (lastIncludeIndex != -1)
  33. {
  34. auto index = lastIncludeIndex;
  35. int numNewLines = 0;
  36. while (++index < lines.size() && lines[index].isEmpty())
  37. ++numNewLines;
  38. if (numNewLines > 1)
  39. lines.removeRange (lastIncludeIndex + 1, numNewLines - 1);
  40. }
  41. }
  42. static String ensureCorrectWhitespace (StringRef input)
  43. {
  44. auto lines = StringArray::fromLines (input);
  45. ensureSingleNewLineAfterIncludes (lines);
  46. return joinLinesIntoSourceFile (lines);
  47. }
  48. static bool isJUCEExample (const File& pipFile)
  49. {
  50. const auto numLinesToTest = 10; // license should be at the top of the file so no need to check all lines
  51. const auto lines = StringArray::fromLines (pipFile.loadFileAsString());
  52. return std::any_of (lines.begin(),
  53. lines.begin() + (std::min (lines.size(), numLinesToTest)),
  54. [] (const auto& line)
  55. {
  56. return line.contains ("This file is part of the JUCE examples.");
  57. });
  58. }
  59. static bool isValidExporterIdentifier (const Identifier& exporterIdentifier)
  60. {
  61. return ProjectExporter::getTypeInfoForExporter (exporterIdentifier).identifier.toString().isNotEmpty();
  62. }
  63. static bool exporterRequiresExampleAssets (const Identifier& exporterIdentifier, const String& projectName)
  64. {
  65. return (exporterIdentifier.toString() == XcodeProjectExporter::getValueTreeTypeNameiOS()
  66. || exporterIdentifier.toString() == AndroidProjectExporter::getValueTreeTypeName())
  67. || (exporterIdentifier.toString() == XcodeProjectExporter::getValueTreeTypeNameMac() && projectName == "AUv3SynthPlugin");
  68. }
  69. //==============================================================================
  70. PIPGenerator::PIPGenerator (const File& pip, const File& output, const File& jucePath, const File& userPath)
  71. : pipFile (pip),
  72. juceModulesPath (jucePath),
  73. userModulesPath (userPath),
  74. metadata (parseJUCEHeaderMetadata (pipFile))
  75. {
  76. if (output != File())
  77. {
  78. outputDirectory = output;
  79. isTemp = false;
  80. }
  81. else
  82. {
  83. outputDirectory = File::getSpecialLocation (File::SpecialLocationType::tempDirectory).getChildFile ("PIPs");
  84. isTemp = true;
  85. }
  86. auto isClipboard = (pip.getParentDirectory().getFileName() == "Clipboard"
  87. && pip.getParentDirectory().getParentDirectory().getFileName() == "PIPs");
  88. outputDirectory = outputDirectory.getChildFile (metadata[Ids::name].toString()).getNonexistentSibling();
  89. useLocalCopy = metadata[Ids::useLocalCopy].toString().trim().getIntValue() == 1 || isClipboard;
  90. if (userModulesPath != File())
  91. {
  92. availableUserModules.reset (new AvailableModulesList());
  93. availableUserModules->scanPaths ({ userModulesPath });
  94. }
  95. }
  96. //==============================================================================
  97. Result PIPGenerator::createJucerFile()
  98. {
  99. ValueTree root (Ids::JUCERPROJECT);
  100. auto result = setProjectSettings (root);
  101. if (result != Result::ok())
  102. return result;
  103. addModules (root);
  104. addExporters (root);
  105. createFiles (root);
  106. setModuleFlags (root);
  107. auto outputFile = outputDirectory.getChildFile (metadata[Ids::name].toString() + ".jucer");
  108. if (auto xml = root.createXml())
  109. if (xml->writeTo (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. void PIPGenerator::addFileToTree (ValueTree& groupTree, const String& name, bool compile, const String& path)
  123. {
  124. ValueTree file (Ids::FILE);
  125. file.setProperty (Ids::ID, createAlphaNumericUID(), nullptr);
  126. file.setProperty (Ids::name, name, nullptr);
  127. file.setProperty (Ids::compile, compile, nullptr);
  128. file.setProperty (Ids::resource, 0, nullptr);
  129. file.setProperty (Ids::file, path, nullptr);
  130. groupTree.addChild (file, -1, nullptr);
  131. }
  132. void PIPGenerator::createFiles (ValueTree& jucerTree)
  133. {
  134. auto sourceDir = outputDirectory.getChildFile ("Source");
  135. if (! sourceDir.exists())
  136. sourceDir.createDirectory();
  137. if (useLocalCopy)
  138. pipFile.copyFileTo (sourceDir.getChildFile (pipFile.getFileName()));
  139. ValueTree mainGroup (Ids::MAINGROUP);
  140. mainGroup.setProperty (Ids::ID, createAlphaNumericUID(), nullptr);
  141. mainGroup.setProperty (Ids::name, metadata[Ids::name], nullptr);
  142. ValueTree group (Ids::GROUP);
  143. group.setProperty (Ids::ID, createGUID (sourceDir.getFullPathName() + "_guidpathsaltxhsdf"), nullptr);
  144. group.setProperty (Ids::name, "Source", nullptr);
  145. addFileToTree (group, "Main.cpp", true, "Source/Main.cpp");
  146. addFileToTree (group, pipFile.getFileName(), false, useLocalCopy ? "Source/" + pipFile.getFileName()
  147. : pipFile.getFullPathName());
  148. mainGroup.addChild (group, -1, nullptr);
  149. if (useLocalCopy)
  150. {
  151. auto relativeFiles = replaceRelativeIncludesAndGetFilesToMove();
  152. if (relativeFiles.size() > 0)
  153. {
  154. ValueTree assets (Ids::GROUP);
  155. assets.setProperty (Ids::ID, createAlphaNumericUID(), nullptr);
  156. assets.setProperty (Ids::name, "Assets", nullptr);
  157. for (auto& f : relativeFiles)
  158. if (copyRelativeFileToLocalSourceDirectory (f))
  159. addFileToTree (assets, f.getFileName(), f.getFileExtension() == ".cpp", "Source/" + f.getFileName());
  160. mainGroup.addChild (assets, -1, nullptr);
  161. }
  162. }
  163. jucerTree.addChild (mainGroup, 0, nullptr);
  164. }
  165. String PIPGenerator::getDocumentControllerClass() const
  166. {
  167. return metadata.getProperty (Ids::documentControllerClass, "").toString();
  168. }
  169. ValueTree PIPGenerator::createModulePathChild (const String& moduleID)
  170. {
  171. ValueTree modulePath (Ids::MODULEPATH);
  172. modulePath.setProperty (Ids::ID, moduleID, nullptr);
  173. modulePath.setProperty (Ids::path, getPathForModule (moduleID), nullptr);
  174. return modulePath;
  175. }
  176. ValueTree PIPGenerator::createBuildConfigChild (bool isDebug)
  177. {
  178. ValueTree child (Ids::CONFIGURATION);
  179. child.setProperty (Ids::name, isDebug ? "Debug" : "Release", nullptr);
  180. child.setProperty (Ids::isDebug, isDebug ? 1 : 0, nullptr);
  181. child.setProperty (Ids::optimisation, isDebug ? 1 : 3, nullptr);
  182. child.setProperty (Ids::targetName, metadata[Ids::name], nullptr);
  183. return child;
  184. }
  185. ValueTree PIPGenerator::createExporterChild (const Identifier& exporterIdentifier)
  186. {
  187. ValueTree exporter (exporterIdentifier);
  188. exporter.setProperty (Ids::targetFolder, "Builds/" + ProjectExporter::getTypeInfoForExporter (exporterIdentifier).targetFolder, nullptr);
  189. if (isJUCEExample (pipFile) && exporterRequiresExampleAssets (exporterIdentifier, metadata[Ids::name]))
  190. {
  191. auto examplesDir = getExamplesDirectory();
  192. if (examplesDir != File())
  193. {
  194. auto assetsDirectoryPath = examplesDir.getChildFile ("Assets").getFullPathName();
  195. exporter.setProperty (exporterIdentifier.toString() == AndroidProjectExporter::getValueTreeTypeName() ? Ids::androidExtraAssetsFolder
  196. : Ids::customXcodeResourceFolders,
  197. assetsDirectoryPath, nullptr);
  198. }
  199. else
  200. {
  201. // invalid JUCE path
  202. jassertfalse;
  203. }
  204. }
  205. if (exporterIdentifier.toString() == AndroidProjectExporter::getValueTreeTypeName())
  206. exporter.setProperty (Ids::androidBluetoothNeeded, true, nullptr);
  207. {
  208. ValueTree configs (Ids::CONFIGURATIONS);
  209. configs.addChild (createBuildConfigChild (true), -1, nullptr);
  210. configs.addChild (createBuildConfigChild (false), -1, nullptr);
  211. exporter.addChild (configs, -1, nullptr);
  212. }
  213. {
  214. ValueTree modulePaths (Ids::MODULEPATHS);
  215. auto modules = StringArray::fromTokens (metadata[Ids::dependencies_].toString(), ",", {});
  216. for (auto m : modules)
  217. modulePaths.addChild (createModulePathChild (m.trim()), -1, nullptr);
  218. exporter.addChild (modulePaths, -1, nullptr);
  219. }
  220. return exporter;
  221. }
  222. ValueTree PIPGenerator::createModuleChild (const String& moduleID)
  223. {
  224. ValueTree module (Ids::MODULE);
  225. module.setProperty (Ids::ID, moduleID, nullptr);
  226. module.setProperty (Ids::showAllCode, 1, nullptr);
  227. module.setProperty (Ids::useLocalCopy, 0, nullptr);
  228. module.setProperty (Ids::useGlobalPath, (getPathForModule (moduleID).isEmpty() ? 1 : 0), nullptr);
  229. return module;
  230. }
  231. void PIPGenerator::addExporters (ValueTree& jucerTree)
  232. {
  233. ValueTree exportersTree (Ids::EXPORTFORMATS);
  234. auto exporters = StringArray::fromTokens (metadata[Ids::exporters].toString(), ",", {});
  235. for (auto& e : exporters)
  236. {
  237. e = e.trim().toUpperCase();
  238. if (isValidExporterIdentifier (e))
  239. exportersTree.addChild (createExporterChild (e), -1, nullptr);
  240. }
  241. jucerTree.addChild (exportersTree, -1, nullptr);
  242. }
  243. void PIPGenerator::addModules (ValueTree& jucerTree)
  244. {
  245. ValueTree modulesTree (Ids::MODULES);
  246. auto modules = StringArray::fromTokens (metadata[Ids::dependencies_].toString(), ",", {});
  247. modules.trim();
  248. for (auto& m : modules)
  249. modulesTree.addChild (createModuleChild (m.trim()), -1, nullptr);
  250. jucerTree.addChild (modulesTree, -1, nullptr);
  251. }
  252. Result PIPGenerator::setProjectSettings (ValueTree& jucerTree)
  253. {
  254. auto setPropertyIfNotEmpty = [&jucerTree] (const Identifier& name, const var& value)
  255. {
  256. if (value != var())
  257. jucerTree.setProperty (name, value, nullptr);
  258. };
  259. setPropertyIfNotEmpty (Ids::name, metadata[Ids::name]);
  260. setPropertyIfNotEmpty (Ids::companyName, metadata[Ids::vendor]);
  261. setPropertyIfNotEmpty (Ids::version, metadata[Ids::version]);
  262. setPropertyIfNotEmpty (Ids::userNotes, metadata[Ids::description]);
  263. setPropertyIfNotEmpty (Ids::companyWebsite, metadata[Ids::website]);
  264. auto defines = metadata[Ids::defines].toString();
  265. if (isJUCEExample (pipFile))
  266. {
  267. auto examplesDir = getExamplesDirectory();
  268. if (examplesDir != File())
  269. {
  270. defines += ((defines.isEmpty() ? "" : " ") + String ("PIP_JUCE_EXAMPLES_DIRECTORY=")
  271. + Base64::toBase64 (examplesDir.getFullPathName()));
  272. }
  273. else
  274. {
  275. return Result::fail (String ("Invalid JUCE path. Set path to JUCE via ") +
  276. (TargetOS::getThisOS() == TargetOS::osx ? "\"Projucer->Global Paths...\""
  277. : "\"File->Global Paths...\"")
  278. + " menu item.");
  279. }
  280. jucerTree.setProperty (Ids::displaySplashScreen, true, nullptr);
  281. }
  282. setPropertyIfNotEmpty (Ids::defines, defines);
  283. auto type = metadata[Ids::type].toString();
  284. if (type == "Console")
  285. {
  286. jucerTree.setProperty (Ids::projectType, build_tools::ProjectType_ConsoleApp::getTypeName(), nullptr);
  287. }
  288. else if (type == "Component")
  289. {
  290. jucerTree.setProperty (Ids::projectType, build_tools::ProjectType_GUIApp::getTypeName(), nullptr);
  291. }
  292. else if (type == "AudioProcessor")
  293. {
  294. jucerTree.setProperty (Ids::projectType, build_tools::ProjectType_AudioPlugin::getTypeName(), nullptr);
  295. jucerTree.setProperty (Ids::pluginAUIsSandboxSafe, "1", nullptr);
  296. setPropertyIfNotEmpty (Ids::pluginManufacturer, metadata[Ids::vendor]);
  297. StringArray pluginFormatsToBuild (Ids::buildVST3.toString(), Ids::buildAU.toString(), Ids::buildStandalone.toString());
  298. pluginFormatsToBuild.addArray (getExtraPluginFormatsToBuild());
  299. if (getDocumentControllerClass().isNotEmpty())
  300. pluginFormatsToBuild.add (Ids::enableARA.toString());
  301. jucerTree.setProperty (Ids::pluginFormats, pluginFormatsToBuild.joinIntoString (","), nullptr);
  302. const auto characteristics = metadata[Ids::pluginCharacteristics].toString();
  303. if (characteristics.isNotEmpty())
  304. jucerTree.setProperty (Ids::pluginCharacteristicsValue,
  305. characteristics.removeCharacters (" \t\n\r"),
  306. nullptr);
  307. }
  308. jucerTree.setProperty (Ids::useAppConfig, false, nullptr);
  309. jucerTree.setProperty (Ids::addUsingNamespaceToJuceHeader, true, nullptr);
  310. return Result::ok();
  311. }
  312. void PIPGenerator::setModuleFlags (ValueTree& jucerTree)
  313. {
  314. ValueTree options ("JUCEOPTIONS");
  315. for (auto& option : StringArray::fromTokens (metadata[Ids::moduleFlags].toString(), ",", {}))
  316. {
  317. auto name = option.upToFirstOccurrenceOf ("=", false, true).trim();
  318. auto value = option.fromFirstOccurrenceOf ("=", false, true).trim();
  319. options.setProperty (name, (value == "1" ? 1 : 0), nullptr);
  320. }
  321. if (metadata[Ids::type].toString() == "AudioProcessor"
  322. && options.getPropertyPointer ("JUCE_VST3_CAN_REPLACE_VST2") == nullptr)
  323. options.setProperty ("JUCE_VST3_CAN_REPLACE_VST2", 0, nullptr);
  324. jucerTree.addChild (options, -1, nullptr);
  325. }
  326. String PIPGenerator::getMainFileTextForType()
  327. {
  328. const auto type = metadata[Ids::type].toString();
  329. const auto documentControllerClass = getDocumentControllerClass();
  330. const auto mainTemplate = [&]
  331. {
  332. if (type == "Console")
  333. return String (BinaryData::PIPConsole_cpp_in);
  334. if (type == "Component")
  335. return String (BinaryData::PIPComponent_cpp_in)
  336. .replace ("${JUCE_PIP_NAME}", metadata[Ids::name].toString())
  337. .replace ("${PROJECT_VERSION}", metadata[Ids::version].toString())
  338. .replace ("${JUCE_PIP_MAIN_CLASS}", metadata[Ids::mainClass].toString());
  339. if (type == "AudioProcessor")
  340. {
  341. if (documentControllerClass.isNotEmpty())
  342. {
  343. return String (BinaryData::PIPAudioProcessorWithARA_cpp_in)
  344. .replace ("${JUCE_PIP_MAIN_CLASS}", metadata[Ids::mainClass].toString())
  345. .replace ("${JUCE_PIP_DOCUMENTCONTROLLER_CLASS}", documentControllerClass);
  346. }
  347. return String (BinaryData::PIPAudioProcessor_cpp_in)
  348. .replace ("${JUCE_PIP_MAIN_CLASS}", metadata[Ids::mainClass].toString());
  349. }
  350. return String{};
  351. }();
  352. if (mainTemplate.isEmpty())
  353. return {};
  354. const auto includeFilename = [&]
  355. {
  356. if (useLocalCopy) return pipFile.getFileName();
  357. if (isTemp) return pipFile.getFullPathName();
  358. return build_tools::RelativePath (pipFile,
  359. outputDirectory.getChildFile ("Source"),
  360. build_tools::RelativePath::unknown).toUnixStyle();
  361. }();
  362. return ensureCorrectWhitespace (mainTemplate.replace ("${JUCE_PIP_HEADER}", includeFilename));
  363. }
  364. //==============================================================================
  365. Array<File> PIPGenerator::replaceRelativeIncludesAndGetFilesToMove()
  366. {
  367. StringArray lines;
  368. pipFile.readLines (lines);
  369. Array<File> files;
  370. for (auto& line : lines)
  371. {
  372. if (line.contains ("#include") && ! line.contains ("JuceLibraryCode"))
  373. {
  374. auto path = line.fromFirstOccurrenceOf ("#include", false, false);
  375. path = path.removeCharacters ("\"").trim();
  376. if (path.startsWith ("<") && path.endsWith (">"))
  377. continue;
  378. auto file = pipFile.getParentDirectory().getChildFile (path);
  379. files.add (file);
  380. line = line.replace (path, file.getFileName());
  381. }
  382. }
  383. outputDirectory.getChildFile ("Source")
  384. .getChildFile (pipFile.getFileName())
  385. .replaceWithText (joinLinesIntoSourceFile (lines));
  386. return files;
  387. }
  388. bool PIPGenerator::copyRelativeFileToLocalSourceDirectory (const File& fileToCopy) const noexcept
  389. {
  390. return fileToCopy.copyFileTo (outputDirectory.getChildFile ("Source")
  391. .getChildFile (fileToCopy.getFileName()));
  392. }
  393. StringArray PIPGenerator::getExtraPluginFormatsToBuild() const
  394. {
  395. auto tokens = StringArray::fromTokens (metadata[Ids::extraPluginFormats].toString(), ",", {});
  396. for (auto& token : tokens)
  397. {
  398. token = [&]
  399. {
  400. if (token == "IAA")
  401. return Ids::enableIAA.toString();
  402. return "build" + token;
  403. }();
  404. }
  405. return tokens;
  406. }
  407. String PIPGenerator::getPathForModule (const String& moduleID) const
  408. {
  409. if (isJUCEModule (moduleID))
  410. {
  411. if (juceModulesPath != File())
  412. {
  413. if (isTemp)
  414. return juceModulesPath.getFullPathName();
  415. return build_tools::RelativePath (juceModulesPath,
  416. outputDirectory,
  417. build_tools::RelativePath::projectFolder).toUnixStyle();
  418. }
  419. }
  420. else if (availableUserModules != nullptr)
  421. {
  422. auto moduleRoot = availableUserModules->getModuleWithID (moduleID).second.getParentDirectory();
  423. if (isTemp)
  424. return moduleRoot.getFullPathName();
  425. return build_tools::RelativePath (moduleRoot,
  426. outputDirectory,
  427. build_tools::RelativePath::projectFolder).toUnixStyle();
  428. }
  429. return {};
  430. }
  431. File PIPGenerator::getExamplesDirectory() const
  432. {
  433. if (juceModulesPath != File())
  434. {
  435. auto examples = juceModulesPath.getSiblingFile ("examples");
  436. if (isValidJUCEExamplesDirectory (examples))
  437. return examples;
  438. }
  439. auto examples = File (getAppSettings().getStoredPath (Ids::jucePath, TargetOS::getThisOS()).get().toString()).getChildFile ("examples");
  440. if (isValidJUCEExamplesDirectory (examples))
  441. return examples;
  442. return {};
  443. }