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 namespace juce
27 {
28 namespace build_tools
29 {
30     //==============================================================================
keyFoundAndNotSequentialDuplicate(XmlElement & xml,const String & key)31     static bool keyFoundAndNotSequentialDuplicate (XmlElement& xml, const String& key)
32     {
33         for (auto* element : xml.getChildWithTagNameIterator ("key"))
34         {
35             if (element->getAllSubText().trim().equalsIgnoreCase (key))
36             {
37                 if (element->getNextElement() != nullptr && element->getNextElement()->hasTagName ("key"))
38                 {
39                     // found broken plist format (sequential duplicate), fix by removing
40                     xml.removeChildElement (element, true);
41                     return false;
42                 }
43 
44                 // key found (not sequential duplicate)
45                 return true;
46             }
47         }
48 
49         // key not found
50         return false;
51     }
52 
addKeyIfNotFound(XmlElement & xml,const String & key)53     static bool addKeyIfNotFound (XmlElement& xml, const String& key)
54     {
55         if (! keyFoundAndNotSequentialDuplicate (xml, key))
56         {
57             xml.createNewChildElement ("key")->addTextElement (key);
58             return true;
59         }
60 
61         return false;
62     }
63 
addPlistDictionaryKey(XmlElement & xml,const String & key,const String & value)64     static void addPlistDictionaryKey (XmlElement& xml, const String& key, const String& value)
65     {
66         if (addKeyIfNotFound (xml, key))
67             xml.createNewChildElement ("string")->addTextElement (value);
68     }
69 
70     template <size_t N>
addPlistDictionaryKey(XmlElement & xml,const String & key,const char (& value)[N])71     static void addPlistDictionaryKey (XmlElement& xml, const String& key, const char (&value) [N])
72     {
73         addPlistDictionaryKey (xml, key, String { value });
74     }
75 
addPlistDictionaryKey(XmlElement & xml,const String & key,const bool value)76     static void addPlistDictionaryKey (XmlElement& xml, const String& key, const bool value)
77     {
78         if (addKeyIfNotFound (xml, key))
79             xml.createNewChildElement (value ? "true" : "false");
80     }
81 
addPlistDictionaryKey(XmlElement & xml,const String & key,int value)82     static void addPlistDictionaryKey (XmlElement& xml, const String& key, int value)
83     {
84         if (addKeyIfNotFound (xml, key))
85             xml.createNewChildElement ("integer")->addTextElement (String (value));
86     }
87 
88     //==============================================================================
addArrayToPlist(XmlElement & dict,String arrayKey,const StringArray & arrayElements)89     static void addArrayToPlist (XmlElement& dict, String arrayKey, const StringArray& arrayElements)
90     {
91         dict.createNewChildElement ("key")->addTextElement (arrayKey);
92         auto* plistStringArray = dict.createNewChildElement ("array");
93 
94         for (auto& e : arrayElements)
95             plistStringArray->createNewChildElement ("string")->addTextElement (e);
96     }
97 
write(const File & infoPlistFile) const98     void PlistOptions::write (const File& infoPlistFile) const
99     {
100         writeStreamToFile (infoPlistFile, [&] (MemoryOutputStream& mo) { write (mo); });
101     }
102 
write(MemoryOutputStream & mo) const103     void PlistOptions::write (MemoryOutputStream& mo) const
104     {
105         XmlElement::TextFormat format;
106         format.dtd = "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">";
107         createXML()->writeTo (mo, format);
108     }
109 
createXML() const110     std::unique_ptr<XmlElement> PlistOptions::createXML() const
111     {
112         auto plist = parseXML (plistToMerge);
113 
114         if (plist == nullptr || ! plist->hasTagName ("plist"))
115             plist.reset (new XmlElement ("plist"));
116 
117         auto* dict = plist->getChildByName ("dict");
118 
119         if (dict == nullptr)
120             dict = plist->createNewChildElement ("dict");
121 
122         if (microphonePermissionEnabled)
123             addPlistDictionaryKey (*dict, "NSMicrophoneUsageDescription", microphonePermissionText);
124 
125         if (cameraPermissionEnabled)
126             addPlistDictionaryKey (*dict, "NSCameraUsageDescription", cameraPermissionText);
127 
128         if (bluetoothPermissionEnabled)
129             addPlistDictionaryKey (*dict, "NSBluetoothAlwaysUsageDescription", bluetoothPermissionText);
130 
131         if (iOS)
132         {
133             if (bluetoothPermissionEnabled)
134                 addPlistDictionaryKey (*dict, "NSBluetoothPeripheralUsageDescription", bluetoothPermissionText); // needed for pre iOS 13.0
135 
136             addPlistDictionaryKey (*dict, "LSRequiresIPhoneOS", true);
137             addPlistDictionaryKey (*dict, "UIViewControllerBasedStatusBarAppearance", true);
138 
139             if (shouldAddStoryboardToProject)
140                 addPlistDictionaryKey (*dict, "UILaunchStoryboardName", storyboardName);
141         }
142         else
143         {
144             if (sendAppleEventsPermissionEnabled)
145                 addPlistDictionaryKey (*dict, "NSAppleEventsUsageDescription", sendAppleEventsPermissionText);
146         }
147 
148         addPlistDictionaryKey (*dict, "CFBundleExecutable",          executableName);
149 
150         if (! iOS) // (NB: on iOS this causes error ITMS-90032 during publishing)
151             addPlistDictionaryKey (*dict, "CFBundleIconFile", iconFile.exists() ? iconFile.getFileName() : String());
152 
153         addPlistDictionaryKey (*dict, "CFBundleIdentifier",          bundleIdentifier);
154         addPlistDictionaryKey (*dict, "CFBundleName",                projectName);
155 
156         // needed by NSExtension on iOS
157         addPlistDictionaryKey (*dict, "CFBundleDisplayName",         projectName);
158         addPlistDictionaryKey (*dict, "CFBundlePackageType",         getXcodePackageType (type));
159         addPlistDictionaryKey (*dict, "CFBundleSignature",           getXcodeBundleSignature (type));
160         addPlistDictionaryKey (*dict, "CFBundleShortVersionString",  version);
161         addPlistDictionaryKey (*dict, "CFBundleVersion",             version);
162         addPlistDictionaryKey (*dict, "NSHumanReadableCopyright",    companyCopyright);
163         addPlistDictionaryKey (*dict, "NSHighResolutionCapable", true);
164 
165         auto replacedDocExtensions = StringArray::fromTokens (replacePreprocessorDefs (allPreprocessorDefs,
166                                                                                        documentExtensions), ",", {});
167         replacedDocExtensions.trim();
168         replacedDocExtensions.removeEmptyStrings (true);
169 
170         if (! replacedDocExtensions.isEmpty() && type != ProjectType::Target::AudioUnitv3PlugIn)
171         {
172             dict->createNewChildElement ("key")->addTextElement ("CFBundleDocumentTypes");
173             auto* dict2 = dict->createNewChildElement ("array")->createNewChildElement ("dict");
174             XmlElement* arrayTag = nullptr;
175 
176             for (auto ex : replacedDocExtensions)
177             {
178                 if (ex.startsWithChar ('.'))
179                     ex = ex.substring (1);
180 
181                 if (arrayTag == nullptr)
182                 {
183                     dict2->createNewChildElement ("key")->addTextElement ("CFBundleTypeExtensions");
184                     arrayTag = dict2->createNewChildElement ("array");
185 
186                     addPlistDictionaryKey (*dict2, "CFBundleTypeName", ex);
187                     addPlistDictionaryKey (*dict2, "CFBundleTypeRole", "Editor");
188                     addPlistDictionaryKey (*dict2, "CFBundleTypeIconFile", "Icon");
189                     addPlistDictionaryKey (*dict2, "NSPersistentStoreTypeKey", "XML");
190                 }
191 
192                 arrayTag->createNewChildElement ("string")->addTextElement (ex);
193             }
194         }
195 
196         if (fileSharingEnabled && type != ProjectType::Target::AudioUnitv3PlugIn)
197             addPlistDictionaryKey (*dict, "UIFileSharingEnabled", true);
198 
199         if (documentBrowserEnabled)
200             addPlistDictionaryKey (*dict, "UISupportsDocumentBrowser", true);
201 
202         if (iOS)
203         {
204             if (type != ProjectType::Target::AudioUnitv3PlugIn)
205             {
206                 if (statusBarHidden)
207                     addPlistDictionaryKey (*dict, "UIStatusBarHidden", true);
208 
209                 addPlistDictionaryKey (*dict, "UIRequiresFullScreen", requiresFullScreen);
210 
211                 addIosScreenOrientations (*dict);
212                 addIosBackgroundModes (*dict);
213             }
214 
215             if (type == ProjectType::Target::StandalonePlugIn && enableIAA)
216             {
217                 XmlElement audioComponentsPlistKey ("key");
218                 audioComponentsPlistKey.addTextElement ("AudioComponents");
219 
220                 dict->addChildElement (new XmlElement (audioComponentsPlistKey));
221 
222                 XmlElement audioComponentsPlistEntry ("array");
223                 auto* audioComponentsDict = audioComponentsPlistEntry.createNewChildElement ("dict");
224 
225                 addPlistDictionaryKey (*audioComponentsDict, "name",         IAAPluginName);
226                 addPlistDictionaryKey (*audioComponentsDict, "manufacturer", pluginManufacturerCode.substring (0, 4));
227                 addPlistDictionaryKey (*audioComponentsDict, "type",         IAATypeCode);
228                 addPlistDictionaryKey (*audioComponentsDict, "subtype",      pluginCode.substring (0, 4));
229                 addPlistDictionaryKey (*audioComponentsDict, "version",      versionAsHex);
230 
231                 dict->addChildElement (new XmlElement (audioComponentsPlistEntry));
232             }
233         }
234 
235         const auto extraOptions = [&]() -> Array<XmlElement>
236         {
237             if (type == ProjectType::Target::Type::AudioUnitPlugIn)
238                 return createExtraAudioUnitTargetPlistOptions();
239 
240             if (type == ProjectType::Target::Type::AudioUnitv3PlugIn)
241                 return createExtraAudioUnitV3TargetPlistOptions();
242 
243             return {};
244         }();
245 
246         for (auto& e : extraOptions)
247             dict->addChildElement (new XmlElement (e));
248 
249         return plist;
250     }
251 
addIosScreenOrientations(XmlElement & dict) const252     void PlistOptions::addIosScreenOrientations (XmlElement& dict) const
253     {
254         addArrayToPlist (dict, "UISupportedInterfaceOrientations", iPhoneScreenOrientations);
255 
256         if (iPadScreenOrientations != iPhoneScreenOrientations)
257             addArrayToPlist (dict, "UISupportedInterfaceOrientations~ipad", iPadScreenOrientations);
258     }
259 
addIosBackgroundModes(XmlElement & dict) const260     void PlistOptions::addIosBackgroundModes (XmlElement& dict) const
261     {
262         StringArray iosBackgroundModes;
263         if (backgroundAudioEnabled)     iosBackgroundModes.add ("audio");
264         if (backgroundBleEnabled)       iosBackgroundModes.add ("bluetooth-central");
265         if (pushNotificationsEnabled)   iosBackgroundModes.add ("remote-notification");
266 
267         addArrayToPlist (dict, "UIBackgroundModes", iosBackgroundModes);
268     }
269 
createExtraAudioUnitTargetPlistOptions() const270     Array<XmlElement> PlistOptions::createExtraAudioUnitTargetPlistOptions() const
271     {
272         XmlElement plistKey ("key");
273         plistKey.addTextElement ("AudioComponents");
274 
275         XmlElement plistEntry ("array");
276         auto* dict = plistEntry.createNewChildElement ("dict");
277 
278         auto truncatedCode = pluginManufacturerCode.substring (0, 4);
279         auto pluginSubType = pluginCode.substring (0, 4);
280 
281         if (truncatedCode.toLowerCase() == truncatedCode)
282         {
283             throw SaveError ("AudioUnit plugin code identifiers invalid!\n\n"
284                              "You have used only lower case letters in your AU plugin manufacturer identifier. "
285                              "You must have at least one uppercase letter in your AU plugin manufacturer "
286                              "identifier code.");
287         }
288 
289         addPlistDictionaryKey (*dict, "name", pluginManufacturer + ": " + pluginName);
290         addPlistDictionaryKey (*dict, "description", pluginDescription);
291         addPlistDictionaryKey (*dict, "factoryFunction", pluginAUExportPrefix + "Factory");
292         addPlistDictionaryKey (*dict, "manufacturer", truncatedCode);
293         addPlistDictionaryKey (*dict, "type", auMainType.removeCharacters ("'"));
294         addPlistDictionaryKey (*dict, "subtype", pluginSubType);
295         addPlistDictionaryKey (*dict, "version", versionAsHex);
296 
297         if (isAuSandboxSafe)
298         {
299             addPlistDictionaryKey (*dict, "sandboxSafe", true);
300         }
301         else if (! suppressResourceUsage)
302         {
303             dict->createNewChildElement ("key")->addTextElement ("resourceUsage");
304             auto* resourceUsageDict = dict->createNewChildElement ("dict");
305 
306             addPlistDictionaryKey (*resourceUsageDict, "network.client", true);
307             addPlistDictionaryKey (*resourceUsageDict, "temporary-exception.files.all.read-write", true);
308         }
309 
310         return { plistKey, plistEntry };
311     }
312 
createExtraAudioUnitV3TargetPlistOptions() const313     Array<XmlElement> PlistOptions::createExtraAudioUnitV3TargetPlistOptions() const
314     {
315         XmlElement plistKey ("key");
316         plistKey.addTextElement ("NSExtension");
317 
318         XmlElement plistEntry ("dict");
319 
320         addPlistDictionaryKey (plistEntry, "NSExtensionPrincipalClass", pluginAUExportPrefix + "FactoryAUv3");
321         addPlistDictionaryKey (plistEntry, "NSExtensionPointIdentifier", "com.apple.AudioUnit-UI");
322         plistEntry.createNewChildElement ("key")->addTextElement ("NSExtensionAttributes");
323 
324         auto* dict = plistEntry.createNewChildElement ("dict");
325         dict->createNewChildElement ("key")->addTextElement ("AudioComponents");
326         auto* componentArray = dict->createNewChildElement ("array");
327 
328         auto* componentDict = componentArray->createNewChildElement ("dict");
329 
330         addPlistDictionaryKey (*componentDict, "name", pluginManufacturer + ": " + pluginName);
331         addPlistDictionaryKey (*componentDict, "description", pluginDescription);
332         addPlistDictionaryKey (*componentDict, "factoryFunction", pluginAUExportPrefix + "FactoryAUv3");
333         addPlistDictionaryKey (*componentDict, "manufacturer", pluginManufacturerCode.substring (0, 4));
334         addPlistDictionaryKey (*componentDict, "type", auMainType.removeCharacters ("'"));
335         addPlistDictionaryKey (*componentDict, "subtype", pluginCode.substring (0, 4));
336         addPlistDictionaryKey (*componentDict, "version", versionAsHex);
337         addPlistDictionaryKey (*componentDict, "sandboxSafe", true);
338 
339         componentDict->createNewChildElement ("key")->addTextElement ("tags");
340         auto* tagsArray = componentDict->createNewChildElement ("array");
341 
342         tagsArray->createNewChildElement ("string")
343             ->addTextElement (isPluginSynth ? "Synth" : "Effects");
344 
345         return { plistKey, plistEntry };
346     }
347 }
348 }
349