/* ============================================================================== This file is part of the JUCE library. Copyright (c) 2017 - ROLI Ltd. JUCE is an open source library subject to commercial or open-source licensing. By using JUCE, you agree to the terms of both the JUCE 5 End-User License Agreement and JUCE 5 Privacy Policy (both updated and effective as of the 27th April 2017). End User License Agreement: www.juce.com/juce-5-licence Privacy Policy: www.juce.com/juce-5-privacy-policy Or: You may also use this code under the terms of the GPL v3 (see www.gnu.org/licenses). JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE DISCLAIMED. ============================================================================== */ #include "../../Application/jucer_Headers.h" #include "../../ProjectSaving/jucer_ProjectExporter.h" #include "jucer_PIPGenerator.h" #include "../../Project/jucer_Module.h" //============================================================================== static String removeEnclosed (const String& input, const String& start, const String& end) { auto startIndex = input.indexOf (start); auto endIndex = input.indexOf (end) + end.length(); if (startIndex != -1 && endIndex != -1) return input.replaceSection (startIndex, endIndex - startIndex, {}); return input; } static void ensureSingleNewLineAfterIncludes (StringArray& lines) { int lastIncludeIndex = -1; for (int i = 0; i < lines.size(); ++i) { if (lines[i].contains ("#include")) lastIncludeIndex = i; } if (lastIncludeIndex != -1) { auto index = lastIncludeIndex; int numNewLines = 0; while (++index < lines.size() && lines[index].isEmpty()) ++numNewLines; if (numNewLines > 1) lines.removeRange (lastIncludeIndex + 1, numNewLines - 1); } } static String ensureCorrectWhitespace (StringRef input) { auto lines = StringArray::fromLines (input); ensureSingleNewLineAfterIncludes (lines); return joinLinesIntoSourceFile (lines); } static bool isJUCEExample (const File& pipFile) { int numLinesToTest = 10; // license should be at the top of the file so no need to // check all lines for (auto line : StringArray::fromLines (pipFile.loadFileAsString())) { if (line.contains ("This file is part of the JUCE examples.")) return true; --numLinesToTest; } return false; } static bool isValidExporterName (StringRef exporterName) { return ProjectExporter::getExporterValueTreeNames().contains (exporterName, true); } static bool exporterRequiresExampleAssets (const String& exporterName, const String& projectName) { return (exporterName == "XCODE_IPHONE" || exporterName == "ANDROIDSTUDIO") || (exporterName == "XCODE_MAC" && projectName == "AUv3SynthPlugin"); } //============================================================================== PIPGenerator::PIPGenerator (const File& pip, const File& output, const File& jucePath, const File& userPath) : pipFile (pip), juceModulesPath (jucePath), userModulesPath (userPath), metadata (parseJUCEHeaderMetadata (pipFile)) { if (output != File()) { outputDirectory = output; isTemp = false; } else { outputDirectory = File::getSpecialLocation (File::SpecialLocationType::tempDirectory).getChildFile ("PIPs"); isTemp = true; } auto isClipboard = (pip.getParentDirectory().getFileName() == "Clipboard" && pip.getParentDirectory().getParentDirectory().getFileName() == "PIPs"); outputDirectory = outputDirectory.getChildFile (metadata[Ids::name].toString()); useLocalCopy = metadata[Ids::useLocalCopy].toString().trim().getIntValue() == 1 || isClipboard; if (userModulesPath != File()) { availableUserModules.reset (new AvailableModuleList()); availableUserModules->scanPaths ({ userModulesPath }); } } //============================================================================== Result PIPGenerator::createJucerFile() { ValueTree root (Ids::JUCERPROJECT); auto result = setProjectSettings (root); if (result != Result::ok()) return result; addModules (root); addExporters (root); createFiles (root); setModuleFlags (root); auto outputFile = outputDirectory.getChildFile (metadata[Ids::name].toString() + ".jucer"); if (auto xml = root.createXml()) if (xml->writeTo (outputFile, {})) return Result::ok(); return Result::fail ("Failed to create .jucer file in " + outputDirectory.getFullPathName()); } Result PIPGenerator::createMainCpp() { auto outputFile = outputDirectory.getChildFile ("Source").getChildFile ("Main.cpp"); if (! outputFile.existsAsFile() && (outputFile.create() != Result::ok())) return Result::fail ("Failed to create Main.cpp - " + outputFile.getFullPathName()); outputFile.replaceWithText (getMainFileTextForType()); return Result::ok(); } //============================================================================== void PIPGenerator::addFileToTree (ValueTree& groupTree, const String& name, bool compile, const String& path) { ValueTree file (Ids::FILE); file.setProperty (Ids::ID, createAlphaNumericUID(), nullptr); file.setProperty (Ids::name, name, nullptr); file.setProperty (Ids::compile, compile, nullptr); file.setProperty (Ids::resource, 0, nullptr); file.setProperty (Ids::file, path, nullptr); groupTree.addChild (file, -1, nullptr); } void PIPGenerator::createFiles (ValueTree& jucerTree) { auto sourceDir = outputDirectory.getChildFile ("Source"); if (! sourceDir.exists()) sourceDir.createDirectory(); if (useLocalCopy) pipFile.copyFileTo (sourceDir.getChildFile (pipFile.getFileName())); ValueTree mainGroup (Ids::MAINGROUP); mainGroup.setProperty (Ids::ID, createAlphaNumericUID(), nullptr); mainGroup.setProperty (Ids::name, metadata[Ids::name], nullptr); ValueTree group (Ids::GROUP); group.setProperty (Ids::ID, createGUID (sourceDir.getFullPathName() + "_guidpathsaltxhsdf"), nullptr); group.setProperty (Ids::name, "Source", nullptr); addFileToTree (group, "Main.cpp", true, "Source/Main.cpp"); addFileToTree (group, pipFile.getFileName(), false, useLocalCopy ? "Source/" + pipFile.getFileName() : pipFile.getFullPathName()); mainGroup.addChild (group, -1, nullptr); if (useLocalCopy) { auto relativeFiles = replaceRelativeIncludesAndGetFilesToMove(); if (relativeFiles.size() > 0) { ValueTree assets (Ids::GROUP); assets.setProperty (Ids::ID, createAlphaNumericUID(), nullptr); assets.setProperty (Ids::name, "Assets", nullptr); for (auto& f : relativeFiles) if (copyRelativeFileToLocalSourceDirectory (f)) addFileToTree (assets, f.getFileName(), f.getFileExtension() == ".cpp", "Source/" + f.getFileName()); mainGroup.addChild (assets, -1, nullptr); } } jucerTree.addChild (mainGroup, 0, nullptr); } ValueTree PIPGenerator::createModulePathChild (const String& moduleID) { ValueTree modulePath (Ids::MODULEPATH); modulePath.setProperty (Ids::ID, moduleID, nullptr); modulePath.setProperty (Ids::path, getPathForModule (moduleID), nullptr); return modulePath; } ValueTree PIPGenerator::createBuildConfigChild (bool isDebug) { ValueTree child (Ids::CONFIGURATION); child.setProperty (Ids::name, isDebug ? "Debug" : "Release", nullptr); child.setProperty (Ids::isDebug, isDebug ? 1 : 0, nullptr); child.setProperty (Ids::optimisation, isDebug ? 1 : 3, nullptr); child.setProperty (Ids::targetName, metadata[Ids::name], nullptr); return child; } ValueTree PIPGenerator::createExporterChild (const String& exporterName) { ValueTree exporter (exporterName); exporter.setProperty (Ids::targetFolder, "Builds/" + ProjectExporter::getTargetFolderForExporter (exporterName), nullptr); if (isJUCEExample (pipFile) && exporterRequiresExampleAssets (exporterName, metadata[Ids::name])) { auto examplesDir = getExamplesDirectory(); if (examplesDir != File()) { auto assetsDirectoryPath = examplesDir.getChildFile ("Assets").getFullPathName(); exporter.setProperty (exporterName == "ANDROIDSTUDIO" ? Ids::androidExtraAssetsFolder : Ids::customXcodeResourceFolders, assetsDirectoryPath, nullptr); } else { // invalid JUCE path jassertfalse; } } { ValueTree configs (Ids::CONFIGURATIONS); configs.addChild (createBuildConfigChild (true), -1, nullptr); configs.addChild (createBuildConfigChild (false), -1, nullptr); exporter.addChild (configs, -1, nullptr); } { ValueTree modulePaths (Ids::MODULEPATHS); auto modules = StringArray::fromTokens (metadata[Ids::dependencies_].toString(), ",", {}); for (auto m : modules) modulePaths.addChild (createModulePathChild (m.trim()), -1, nullptr); exporter.addChild (modulePaths, -1, nullptr); } return exporter; } ValueTree PIPGenerator::createModuleChild (const String& moduleID) { ValueTree module (Ids::MODULE); module.setProperty (Ids::ID, moduleID, nullptr); module.setProperty (Ids::showAllCode, 1, nullptr); module.setProperty (Ids::useLocalCopy, 0, nullptr); module.setProperty (Ids::useGlobalPath, (getPathForModule (moduleID).isEmpty() ? 1 : 0), nullptr); return module; } void PIPGenerator::addExporters (ValueTree& jucerTree) { ValueTree exportersTree (Ids::EXPORTFORMATS); auto exporters = StringArray::fromTokens (metadata[Ids::exporters].toString(), ",", {}); for (auto& e : exporters) { e = e.trim().toUpperCase(); if (isValidExporterName (e)) exportersTree.addChild (createExporterChild (e), -1, nullptr); } jucerTree.addChild (exportersTree, -1, nullptr); } void PIPGenerator::addModules (ValueTree& jucerTree) { ValueTree modulesTree (Ids::MODULES); auto modules = StringArray::fromTokens (metadata[Ids::dependencies_].toString(), ",", {}); modules.trim(); auto projectType = metadata[Ids::type].toString(); if (projectType == "Console") modules.mergeArray (getModulesRequiredForConsole()); else if (projectType == "Component") modules.mergeArray (getModulesRequiredForComponent()); else if (projectType == "AudioProcessor") modules.mergeArray (getModulesRequiredForAudioProcessor()); for (auto& m : modules) modulesTree.addChild (createModuleChild (m.trim()), -1, nullptr); jucerTree.addChild (modulesTree, -1, nullptr); } Result PIPGenerator::setProjectSettings (ValueTree& jucerTree) { auto setPropertyIfNotEmpty = [&jucerTree] (const Identifier& name, const var& value) { if (value != var()) jucerTree.setProperty (name, value, nullptr); }; setPropertyIfNotEmpty (Ids::name, metadata[Ids::name]); setPropertyIfNotEmpty (Ids::companyName, metadata[Ids::vendor]); setPropertyIfNotEmpty (Ids::version, metadata[Ids::version]); setPropertyIfNotEmpty (Ids::userNotes, metadata[Ids::description]); setPropertyIfNotEmpty (Ids::companyWebsite, metadata[Ids::website]); auto defines = metadata[Ids::defines].toString(); if (isJUCEExample (pipFile)) { auto examplesDir = getExamplesDirectory(); if (examplesDir != File()) { defines += ((defines.isEmpty() ? "" : " ") + String ("PIP_JUCE_EXAMPLES_DIRECTORY=") + Base64::toBase64 (examplesDir.getFullPathName())); } else { return Result::fail (String ("Invalid JUCE path. Set path to JUCE via ") + (TargetOS::getThisOS() == TargetOS::osx ? "\"Projucer->Global Paths...\"" : "\"File->Global Paths...\"") + " menu item."); } } setPropertyIfNotEmpty (Ids::defines, defines); auto type = metadata[Ids::type].toString(); if (type == "Console") { jucerTree.setProperty (Ids::projectType, "consoleapp", nullptr); } else if (type == "Component") { jucerTree.setProperty (Ids::projectType, "guiapp", nullptr); } else if (type == "AudioProcessor") { jucerTree.setProperty (Ids::projectType, "audioplug", nullptr); jucerTree.setProperty (Ids::pluginAUIsSandboxSafe, "1", nullptr); setPropertyIfNotEmpty (Ids::pluginManufacturer, metadata[Ids::vendor]); StringArray pluginFormatsToBuild (Ids::buildVST3.toString(), Ids::buildAU.toString(), Ids::buildStandalone.toString()); pluginFormatsToBuild.addArray (getExtraPluginFormatsToBuild()); jucerTree.setProperty (Ids::pluginFormats, pluginFormatsToBuild.joinIntoString (","), nullptr); if (! getPluginCharacteristics().isEmpty()) jucerTree.setProperty (Ids::pluginCharacteristicsValue, getPluginCharacteristics().joinIntoString (","), nullptr); } return Result::ok(); } void PIPGenerator::setModuleFlags (ValueTree& jucerTree) { ValueTree options ("JUCEOPTIONS"); for (auto& option : StringArray::fromTokens (metadata[Ids::moduleFlags].toString(), ",", {})) { auto name = option.upToFirstOccurrenceOf ("=", false, true).trim(); auto value = option.fromFirstOccurrenceOf ("=", false, true).trim(); options.setProperty (name, (value == "1" ? 1 : 0), nullptr); } if (metadata[Ids::type].toString() == "AudioProcessor" && options.getPropertyPointer ("JUCE_VST3_CAN_REPLACE_VST2") == nullptr) options.setProperty ("JUCE_VST3_CAN_REPLACE_VST2", 0, nullptr); jucerTree.addChild (options, -1, nullptr); } String PIPGenerator::getMainFileTextForType() { String mainTemplate (BinaryData::jucer_PIPMain_cpp); auto includeFilename = [&] { if (useLocalCopy) return pipFile.getFileName(); if (isTemp) return pipFile.getFullPathName(); return RelativePath (pipFile, outputDirectory.getChildFile ("Source"), RelativePath::unknown).toUnixStyle(); }(); mainTemplate = mainTemplate.replace ("%%filename%%", includeFilename) .replace ("%%include_juce%%", CodeHelpers::createIncludePathIncludeStatement (Project::getJuceSourceHFilename())); auto type = metadata[Ids::type].toString(); if (type == "Console") { mainTemplate = removeEnclosed (mainTemplate, "%%component_begin%%", "%%component_end%%"); mainTemplate = removeEnclosed (mainTemplate, "%%audioprocessor_begin%%", "%%audioprocessor_end%%"); mainTemplate = mainTemplate.replace ("%%console_begin%%", {}).replace ("%%console_end%%", {}); return ensureCorrectWhitespace (mainTemplate); } else if (type == "Component") { mainTemplate = removeEnclosed (mainTemplate, "%%audioprocessor_begin%%", "%%audioprocessor_end%%"); mainTemplate = removeEnclosed (mainTemplate, "%%console_begin%%", "%%console_end%%"); mainTemplate = mainTemplate.replace ("%%component_begin%%", {}).replace ("%%component_end%%", {}); mainTemplate = mainTemplate.replace ("%%project_name%%", metadata[Ids::name].toString()); mainTemplate = mainTemplate.replace ("%%project_version%%", metadata[Ids::version].toString()); return ensureCorrectWhitespace (mainTemplate.replace ("%%startup%%", "mainWindow.reset (new MainWindow (" + metadata[Ids::name].toString().quoted() + ", new " + metadata[Ids::mainClass].toString() + "(), *this));") .replace ("%%shutdown%%", "mainWindow = nullptr;")); } else if (type == "AudioProcessor") { mainTemplate = removeEnclosed (mainTemplate, "%%component_begin%%", "%%component_end%%"); mainTemplate = removeEnclosed (mainTemplate, "%%console_begin%%", "%%console_end%%"); mainTemplate = mainTemplate.replace ("%%audioprocessor_begin%%", {}).replace ("%%audioprocessor_end%%", {}); return ensureCorrectWhitespace (mainTemplate.replace ("%%class_name%%", metadata[Ids::mainClass].toString())); } return {}; } //============================================================================== Array PIPGenerator::replaceRelativeIncludesAndGetFilesToMove() { StringArray lines; pipFile.readLines (lines); Array files; for (auto& line : lines) { if (line.contains ("#include") && ! line.contains ("JuceLibraryCode")) { auto path = line.fromFirstOccurrenceOf ("#include", false, false); path = path.removeCharacters ("\"").trim(); if (path.startsWith ("<") && path.endsWith (">")) continue; auto file = pipFile.getParentDirectory().getChildFile (path); files.add (file); line = line.replace (path, file.getFileName()); } } outputDirectory.getChildFile ("Source") .getChildFile (pipFile.getFileName()) .replaceWithText (joinLinesIntoSourceFile (lines)); return files; } bool PIPGenerator::copyRelativeFileToLocalSourceDirectory (const File& fileToCopy) const noexcept { return fileToCopy.copyFileTo (outputDirectory.getChildFile ("Source") .getChildFile (fileToCopy.getFileName())); } StringArray PIPGenerator::getExtraPluginFormatsToBuild() const { auto name = metadata[Ids::name].toString(); if (name == "AUv3SynthPlugin" || name == "AudioPluginDemo") return { Ids::buildAUv3.toString() }; else if (name == "InterAppAudioEffectPlugin") return { Ids::enableIAA.toString() }; return {}; } StringArray PIPGenerator::getPluginCharacteristics() const { auto name = metadata[Ids::name].toString(); if (name == "AudioPluginDemo") return { Ids::pluginIsSynth.toString(), Ids::pluginWantsMidiIn.toString(), Ids::pluginProducesMidiOut.toString(), Ids::pluginEditorRequiresKeys.toString() }; else if (name == "AUv3SynthPlugin" || name == "MultiOutSynthPlugin") return { Ids::pluginIsSynth.toString(), Ids::pluginWantsMidiIn.toString() }; else if (name == "ArpeggiatorPlugin") return { Ids::pluginWantsMidiIn.toString(), Ids::pluginProducesMidiOut.toString(), Ids::pluginIsMidiEffectPlugin.toString() }; return {}; } String PIPGenerator::getPathForModule (const String& moduleID) const { if (isJUCEModule (moduleID)) { if (juceModulesPath != File()) { if (isTemp) return juceModulesPath.getFullPathName(); return RelativePath (juceModulesPath, outputDirectory, RelativePath::projectFolder).toUnixStyle(); } } else if (availableUserModules != nullptr) { auto moduleRoot = availableUserModules->getModuleWithID (moduleID).second.getParentDirectory(); if (isTemp) return moduleRoot.getFullPathName(); return RelativePath (moduleRoot , outputDirectory, RelativePath::projectFolder).toUnixStyle(); } return {}; } File PIPGenerator::getExamplesDirectory() const { if (juceModulesPath != File()) { auto examples = juceModulesPath.getSiblingFile ("examples"); if (isValidJUCEExamplesDirectory (examples)) return examples; } auto examples = File (getAppSettings().getStoredPath (Ids::jucePath, TargetOS::getThisOS()).get().toString()).getChildFile ("examples"); if (isValidJUCEExamplesDirectory (examples)) return examples; return {}; }