1 module plugin;
2 
3 import std.algorithm : any;
4 import std.ascii : isUpper;
5 import std.conv;
6 import std.process;
7 import std..string;
8 import std.file;
9 import std.regex;
10 import std.json;
11 import std.path;
12 import std.stdio;
13 import std.datetime;
14 import std.range;
15 
16 import colorize;
17 import utils;
18 import rsrc;
19 
20 import arch;
21 import dplug.client.daw;
22 
23 enum Compiler
24 {
25     ldc,
26     gdc,
27     dmd,
28 }
29 
30 static if (__VERSION__ >= 2087)
31 {
32     alias jsonTrue = JSONType.true_;
33     alias jsonFalse = JSONType.false_;
34 }
35 else
36 {
37     alias jsonTrue = JSON_TYPE.TRUE;
38     alias jsonFalse = JSON_TYPE.FALSE;
39 }
40 
41 string toString(Compiler compiler)
42 {
43     final switch(compiler) with (Compiler)
44     {
45         case dmd: return "dmd";
46         case gdc: return "gdc";
47         case ldc: return "ldc";
48     }
49 }
50 
51 string toStringUpper(Compiler compiler)
52 {
53     final switch(compiler) with (Compiler)
54     {
55         case dmd: return "DMD";
56         case gdc: return "GDC";
57         case ldc: return "LDC";
58     }
59 }
60 
61 string toStringArchs(Arch[] archs)
62 {
63     string r = "";
64     foreach(size_t i, arch; archs)
65     {
66         final switch(arch) with (Arch)
67         {
68             case x86:
69                 if (i) r ~= " and ";
70                 r ~= "32-bit";
71                 break;
72             case x86_64:
73                 if (i) r ~= " and ";
74                 r ~= "64-bit";
75                 break;
76             case arm32:
77                 if (i) r ~= " and ";
78                 r ~= "arm32";
79                 break;
80             case arm64:
81                 if (i) r ~= " and ";
82                 r ~= "arm64";
83                 break;
84             case universalBinary:
85                 if (i) r ~= " and ";
86                 r ~= "Universal Binary";
87                 break;
88         }
89     }
90     return r;
91 }
92 
93 // from a valid configuration name, extracts the rest of the name.
94 // Typically configuration would be like: "VST-FULL" => "FULL" or "AAX-FREE" => "FREE".
95 // Used for installer file name.
96 string stripConfig(string config) pure nothrow @nogc
97 {
98     if (config.length >= 5 && config[0..5] == "VST3-")
99         return config[5..$];
100     if (config.length >= 5 && config[0..5] == "VST2-")
101         return config[5..$];
102     if (config.length >= 3 && config[0..3] == "AU-")
103         return config[3..$];
104     if (config.length >= 4 && config[0..4] == "AAX-")
105         return config[4..$];
106     if (config.length >= 4 && config[0..4] == "LV2-")
107         return config[4..$];
108     return null;
109 }
110 
111 bool configIsVST3(string config) pure nothrow @nogc
112 {
113     return config.length >= 4 && config[0..4] == "VST3";
114 }
115 
116 bool configIsVST2(string config) pure nothrow @nogc
117 {
118     return config.length >= 4 && config[0..4] == "VST2";
119 }
120 
121 bool configIsAU(string config) pure nothrow @nogc
122 {
123     return config.length >= 2 && config[0..2] == "AU";
124 }
125 
126 bool configIsAAX(string config) pure nothrow @nogc
127 {
128     return config.length >= 3 && config[0..3] == "AAX";
129 }
130 
131 bool configIsLV2(string config) pure nothrow @nogc
132 {
133     return config.length >= 3 && config[0..3] == "LV2";
134 }
135 
136 
137 struct Plugin
138 {
139     string name;       // name, extracted from dub.json(eg: 'distort')
140     string CFBundleIdentifierPrefix;
141     string licensePath;    // can be null
142     string iconPathWindows;       // can be null or a path to a .ico
143     string iconPathOSX;       // can be null or a path to a (large) .png
144     bool hasGUI;
145     string dubTargetPath;  // extracted from dub.json, used to build the dub output file path
146 
147     string pluginName;     // Prettier name, extracted from plugin.json (eg: 'Distorter')
148     string pluginUniqueID;
149     string pluginHomepage;
150     string vendorName;
151     string vendorUniqueID;
152     string vendorSupportEmail;
153 
154     // Available configurations, taken from dub.json
155     string[] configurations;
156 
157     // Public version of the plugin
158     // Each release of a plugin should upgrade the version somehow
159     int publicVersionMajor;
160     int publicVersionMinor;
161     int publicVersionPatch;
162 
163     // The certificate identity to be used for Mac installer or application signing
164     string developerIdentity = null;
165 
166     // relative path to a .png for the Mac installer
167     string installerPNGPath;
168 
169     // relative path to .bmp image for Windows installer header
170     string windowsInstallerHeaderBmp;
171 
172     // Windows-only, points to a .p12/.pfx certificate...
173     // Is needed to codesign anything.
174     private string keyFileWindows;
175 
176     // ...and the password of its private key
177     // Support "!PROMPT" as special value.
178     private string keyPasswordWindows;
179 
180     // <Used for Apple notarization>
181     string vendorAppleID;
182     string appSpecificPassword_altool;
183     string appSpecificPassword_stapler;
184     // </Used for Apple notarization>
185 
186     bool receivesMIDI;
187     bool isSynth;
188 
189     PluginCategory category;
190 
191     string prettyName() pure const nothrow
192     {
193         return vendorName ~ " " ~ pluginName;
194     }
195 
196     string publicVersionString() pure const nothrow
197     {
198         return to!string(publicVersionMajor) ~ "." ~ to!string(publicVersionMinor) ~ "." ~ to!string(publicVersionPatch);
199     }
200 
201     // AU version integer
202     int publicVersionInt() pure const nothrow
203     {
204         return (publicVersionMajor << 16) | (publicVersionMinor << 8) | publicVersionPatch;
205     }
206 
207     string makePkgInfo(string config) pure const nothrow
208     {
209         if (configIsAAX(config))
210             return "TDMwPTul"; // this should actually have no effect on whether or not the AAX plug-ins load
211         else
212             return "BNDL" ~ vendorUniqueID;
213     }
214 
215     string copyright() const  // Copyright information, copied in the OSX bundle
216     {
217         SysTime time = Clock.currTime(UTC());
218         return format("Copyright %s, %s", vendorName, time.year);
219     }
220 
221     // Allows anything permitted in a filename
222     static string sanitizeFilenameString(string s) pure
223     {
224         string r = "";
225         foreach(dchar ch; s)
226         {
227             if (ch >= 'A' && ch <= 'Z')
228                 r ~= ch;
229             else if (ch >= 'a' && ch <= 'z')
230                 r ~= ch;
231             else if (ch >= '0' && ch <= '9')
232                 r ~= ch;
233             else if (ch == '.')
234                 r ~= ch;
235             else
236                 r ~= "-";
237         }
238         return r;
239     }
240 
241     // Make a proper bundle identifier from a string
242     static string sanitizeBundleString(string s) pure
243     {
244         string r = "";
245         foreach(dchar ch; s)
246         {
247             if (ch >= 'A' && ch <= 'Z')
248                 r ~= ch;
249             else if (ch >= 'a' && ch <= 'z')
250                 r ~= ch;
251             else if (ch == '.')
252                 r ~= ch;
253             else
254                 r ~= "-";
255         }
256         return r;
257     }
258 
259     // The file name DUB outputs.
260     // The real filename is found lazily, since DUB may change its method of naming over time,
261     // but we don't want to rely on `dub describe` which has untractable problem with:
262     // `dub describe` being slow on bad network conditions, and `dub describe --skip-registry=all` possibly not terminating
263     // Uses an heuristic for DUB naming, which might get wrong eventually.
264     string dubOutputFileName()
265     {
266         if (dubOutputFileNameCached !is null)
267             return dubOutputFileNameCached;
268 
269         // We assume a build has been made, now find the name of the output file
270 
271         string[] getPotentialPathes()
272         {
273             string[] possiblePathes;
274             version(Windows)
275                 possiblePathes ~= [name  ~ ".dll"];
276             else version(OSX)
277             {
278                 // support multiple DUB versions, this name changed to .dylib in Aug 2018
279                 // newer names goes first to avoid clashes
280                 possiblePathes ~= ["lib" ~ name ~ ".dylib", "lib" ~ name ~ ".so"];
281             }
282             else version(linux)
283                  possiblePathes ~= ["lib" ~ name ~ ".so"];
284             else
285                 static assert(false, "unsupported OS");
286 
287             if (dubTargetPath !is null)
288             {
289                 foreach(ref path; possiblePathes)
290                     path = std.path.buildPath(dubTargetPath, path);
291             }
292             return possiblePathes;
293         }
294 
295         auto possiblePaths = getPotentialPathes();
296 
297         // Find the possible path for which the file exists
298         foreach(path; possiblePaths)
299         {
300             if (std.file.exists(path))
301             {
302                 dubOutputFileNameCached = path;
303                 return path;
304             }
305         }
306         throw new Exception("Didn't found a plug-in file in %s . See dplug-build source to check the heuristic for DUB naming in `dubOutputFileName()`.");
307     }
308     string dubOutputFileNameCached = null;
309 
310     // Gets a config to extract the name of the configuration beyond the prefix
311     string getNotarizationBundleIdentifier(string config) pure const
312     {
313         string verName = stripConfig(config);
314         if (verName)
315             verName = "-" ~ verName;
316         else
317             verName = "";
318         return format("%s.%s%s-%s.pkg",
319                       CFBundleIdentifierPrefix,
320                       sanitizeBundleString(pluginName),
321                       verName,
322                       publicVersionString);
323     }
324 
325     string getVST3BundleIdentifier() pure const
326     {
327         return CFBundleIdentifierPrefix ~ ".vst3." ~ sanitizeBundleString(pluginName);
328     }
329 
330     string getVSTBundleIdentifier() pure const
331     {
332         return CFBundleIdentifierPrefix ~ ".vst." ~ sanitizeBundleString(pluginName);
333     }
334 
335     string getAUBundleIdentifier() pure const
336     {
337         return CFBundleIdentifierPrefix ~ ".audiounit." ~ sanitizeBundleString(pluginName);
338     }
339 
340     string getAAXBundleIdentifier() pure const
341     {
342         return CFBundleIdentifierPrefix ~ ".aax." ~ sanitizeBundleString(pluginName);
343     }
344 
345     string getLV2BundleIdentifier() pure const
346     {
347         return CFBundleIdentifierPrefix ~ ".lv2." ~ sanitizeBundleString(pluginName);
348     }
349 
350     // <Apple specific>
351 
352     // filename of the final installer
353     // give a config to extract a configuration name in case of multiple configurations
354     string finalPkgFilename(string config) pure const
355     {
356         string verName = stripConfig(config);
357         if (verName)
358             verName = "-" ~ verName;
359         else
360             verName = "";
361         return format("%s%s-%s.pkg", sanitizeFilenameString(pluginName),
362                                      verName,
363                                      publicVersionString);
364     }
365 
366     string pkgFilenameVST3() pure const
367     {
368         return sanitizeFilenameString(pluginName) ~ "-vst3.pkg";
369     }
370 
371     string pkgFilenameVST2() pure const
372     {
373         return sanitizeFilenameString(pluginName) ~ "-vst.pkg";
374     }
375 
376     string pkgFilenameAU() pure const
377     {
378         return sanitizeFilenameString(pluginName) ~ "-au.pkg";
379     }
380 
381     string pkgFilenameAAX() pure const
382     {
383         return sanitizeFilenameString(pluginName) ~ "-aax.pkg";
384     }
385 
386     string pkgFilenameLV2() pure const
387     {
388         return sanitizeFilenameString(pluginName) ~ "-lv2.pkg";
389     }
390 
391     string pkgBundleVST3() pure const
392     {
393         return CFBundleIdentifierPrefix ~ "." ~ sanitizeBundleString(pkgFilenameVST3());
394     }
395 
396     string pkgBundleVST2() pure const
397     {
398         return CFBundleIdentifierPrefix ~ "." ~ sanitizeBundleString(pkgFilenameVST2());
399     }
400 
401     string pkgBundleAU() pure const
402     {
403         return CFBundleIdentifierPrefix ~ "." ~ sanitizeBundleString(pkgFilenameAU());
404     }
405 
406     string pkgBundleAAX() pure const
407     {
408         return CFBundleIdentifierPrefix ~ "." ~ sanitizeBundleString(pkgFilenameAAX());
409     }
410 
411     string pkgBundleLV2() pure const
412     {
413         return CFBundleIdentifierPrefix ~ "." ~ sanitizeBundleString(pkgFilenameLV2());
414     }
415 
416     string getAppleID()
417     {
418         if (vendorAppleID is null)
419             throw new Exception(`Missing "vendorAppleID" in plugin.json. Notarization need this key.`);
420         return vendorAppleID;
421     }
422 
423     string getDeveloperIdentity()
424     {
425         if (developerIdentity is null)
426             throw new Exception(`Missing "developerIdentity-osx" in plugin.json`);
427         return developerIdentity;
428     }
429 
430     // </Apple specific>
431 
432     // <Windows specific>
433 
434     string windowsInstallerName(string config) pure const
435     {
436         string verName = stripConfig(config);
437         if(verName)
438             verName = "-" ~ verName;
439         else
440             verName = "";
441         return format("%s%s-%s.exe", sanitizeFilenameString(pluginName), verName, publicVersionString);
442     }
443 
444     string getKeyFileWindows()
445     {
446         if (keyFileWindows is null)
447             throw new Exception(`Missing "keyFile-windows" in plugin.json`);
448         return keyFileWindows;
449     }
450 
451     string getKeyPasswordWindows()
452     {
453         promptWindowsKeyFilePasswordLazily();
454         if (keyPasswordWindows is null)
455             throw new Exception(`Missing "keyPassword-windows" in plugin.json (Recommended value: "!PROMPT" or "$ENVVAR")`);
456         return expandDplugVariables(keyPasswordWindows);
457     }
458 
459     // </Windows specific>
460 
461 
462     // <PACE Ilok specific>
463 
464     /// The iLok account of the AAX developer    
465     private string iLokAccount;    
466 
467     /// The iLok password of the AAX developer (special value "!PROMPT")
468     private string iLokPassword;    
469 
470     /// The wrap configuration GUID (go to PACE Central to create such wrap configurations)
471     private string wrapConfigGUID;  
472 
473     string getILokAccount()
474     {
475         if (iLokAccount is null)
476             throw new Exception(`Missing "iLokAccount" in plugin.json (Note: pace.json has moved to plugin.json, see Dplug's Release Notes)`);
477         return expandDplugVariables(iLokAccount);
478     }
479 
480     string getILokPassword()
481     {
482         promptIlokPasswordLazily();
483         if (iLokPassword is null)
484             throw new Exception(`Missing "iLokPassword" in plugin.json (Recommended value: "!PROMPT" or "$ENVVAR")`);
485         return expandDplugVariables(iLokPassword);
486     }
487 
488     string getWrapConfigGUID()
489     {
490         if (wrapConfigGUID is null)
491             throw new Exception(`Missing "wrapConfigGUID" in plugin.json`);
492         return expandDplugVariables(wrapConfigGUID);
493     }
494 
495     // </PACE Ilok specific>
496 
497 
498     string getLV2PrettyName()
499     {
500         // Note: Carla doesn't support IRI with escaped character, so we have to remove
501         // spaces in LV2 else the binaries aren't found.
502         // This function is only used for the final binary name.
503         // See_also: the LV2 client.
504         return prettyName.replace(" ", "");
505     }
506 
507     string getFirstConfiguration()
508     {
509         if (configurations.length == 0)
510             throw new Exception("Missing configurations, can't build");
511         return configurations[0];
512     }
513 
514     bool configExists(string config)
515     {
516         foreach(c; configurations)
517             if (config == c)
518                 return true;
519         return false;
520     }
521 
522     string[] getMatchingConfigurations(string pattern)
523     {
524         string[] results;
525 
526         auto reg = regex(pattern);
527         foreach(c; configurations)
528         {
529             if (c == pattern)
530             {
531                 results ~= c;
532             }
533         }
534         if (results.length == 0)
535         {
536             string availConfig = format("%s", configurations);
537             throw new Exception(format("No configuration matches: '%s'. Available: %s", pattern, availConfig.yellow));
538         }
539         return results;
540     }
541 
542     void vst3RelatedChecks()
543     {
544         if (vendorSupportEmail is null)
545             warning(`Missing "vendorSupportEmail" in plugin.json. Email address will be wrong in VST3 format.`);
546         if (pluginHomepage is null)
547             warning(`Missing "pluginHomepage" in plugin.json. Plugin homepage will be wrong in VST3 format.`);
548     }
549 
550 
551     private void promptIlokPasswordLazily()
552     {
553         if (iLokPassword == "!PROMPT")
554         {
555             cwriteln();
556             cwritefln(`Please enter your iLok password (seen "!PROMPT"):`.cyan);
557             iLokPassword = chomp(readln());
558             cwriteln();
559         }
560     }
561 
562     private void promptWindowsKeyFilePasswordLazily()
563     {   
564         if (keyPasswordWindows == "!PROMPT")
565         {
566             cwriteln();
567             cwritefln(`Please enter your certificate Windows password (seen "!PROMPT"):`.cyan);
568             keyPasswordWindows = chomp(readln());
569             cwriteln();
570         }
571     }
572 }
573 
574 
575 class DplugBuildBuiltCorrectlyException : Exception
576 {
577     public
578     {
579         @safe pure nothrow this(string message,
580                                 string file =__FILE__,
581                                 size_t line = __LINE__,
582                                 Throwable next = null)
583         {
584             super(message, file, line, next);
585         }
586     }
587 }
588 
589 Plugin readPluginDescription()
590 {
591     if (!exists("dub.json"))
592         throw new Exception("Needs a dub.json file. Please launch 'dplug-build' in a plug-in project directory.");
593 
594     Plugin result;
595 
596     enum useDubDescribe = true;
597 
598     // Open an eventual plugin.json directly to find keys that DUB doesn't bypass
599     JSONValue dubFile = parseJSON(cast(string)(std.file.read("dub.json")));
600 
601     try
602     {
603         result.name = dubFile["name"].str;
604     }
605     catch(Exception e)
606     {
607         throw new Exception("Missing \"name\" in dub.json (eg: \"myplugin\")");
608     }
609 
610     // We simply launched `dub` to build dplug-build. So we're not building a plugin.
611     // avoid the embarassment of having a red message that confuses new users.
612     // You've read correctly: you can't name your plugin "dplug-build" as a consequence.
613     if (result.name == "dplug-build")
614     {
615         throw new DplugBuildBuiltCorrectlyException("");
616     }
617 
618     try
619     {
620         JSONValue[] config = dubFile["configurations"].array();
621 
622         foreach(c; config)
623         {
624             string cname = c["name"].str;
625             if (!configIsAAX(cname)
626               &&!configIsVST2(cname)
627               &&!configIsVST3(cname)
628               &&!configIsAU(cname)
629               &&!configIsLV2(cname))
630                 throw new Exception(format("Configuration name should start with \"VST2\", \"VST3\", \"AU\", \"AAX\", or \"LV2\". '%s' is not a valid configuration name.", cname));
631             result.configurations ~= cname;
632         }
633 
634         // Check configuration names, they must be valid
635 
636     }
637     catch(Exception e)
638     {
639         warning("Couldln't parse configurations names in dub.json.");
640         result.configurations = [];
641     }
642 
643     // Support for DUB targetPath
644     try
645     {
646         JSONValue path = dubFile["targetPath"];
647         result.dubTargetPath = path.str;
648     }
649     catch(Exception e)
650     {
651         // silent, targetPath not considered
652         result.dubTargetPath = null;
653     }
654 
655     if (!exists("plugin.json"))
656         throw new Exception("Needs a plugin.json file for proper bundling. Please create one next to dub.json.");
657 
658     // Open an eventual plugin.json directly to find keys that DUB doesn't bypass
659     JSONValue rawPluginFile = parseJSON(cast(string)(std.file.read("plugin.json")));
660 
661     // Optional keys
662 
663     // prettyName is the fancy Manufacturer + Product name that will be displayed as much as possible in:
664     // - bundle name
665     // - renamed executable file names
666     try
667     {
668         result.pluginName = rawPluginFile["pluginName"].str;
669     }
670     catch(Exception e)
671     {
672         info("Missing \"pluginName\" in plugin.json (eg: \"My Compressor\")\n        => Using dub.json \"name\" key instead.");
673         result.pluginName = result.name;
674     }
675 
676     // TODO: not all characters are allowed in pluginName.
677     //       All characters in pluginName should be able to be in a filename.
678     //       For Orion compatibility is should not have '-' in the file name
679 
680     // Note: dplug-build parses it but doesn't need hasGUI
681     try
682     {
683         result.hasGUI = toBool(rawPluginFile["hasGUI"]);
684     }
685     catch(Exception e)
686     {
687         warning("Missing \"hasGUI\" in plugin.json (must be true or false)\n    => Using false instead.");
688         result.hasGUI = false;
689     }
690 
691     try
692     {
693         string userManualPath = rawPluginFile["userManualPath"].str;
694         warning("\"userManualPath\" is deprecated");
695     }
696     catch(Exception e)
697     {
698     }
699 
700     try
701     {
702         result.licensePath = rawPluginFile["licensePath"].str;
703     }
704     catch(Exception e)
705     {
706         info("Missing \"licensePath\" in plugin.json (eg: \"license.txt\")");
707     }
708 
709     try
710     {
711         result.developerIdentity = rawPluginFile["developerIdentity-osx"].str;
712     }
713     catch(Exception e)
714     {
715         result.developerIdentity = null;
716     }
717 
718     try
719     {
720         result.installerPNGPath = rawPluginFile["installerPNGPath"].str;
721     }
722     catch(Exception e)
723     {
724         result.installerPNGPath = null;
725     }
726 
727     try
728     {
729         result.windowsInstallerHeaderBmp = rawPluginFile["windowsInstallerHeaderBmp"].str;
730     }
731     catch(Exception e)
732     {
733         result.windowsInstallerHeaderBmp = null;
734     }
735 
736     try
737     {
738         result.keyFileWindows = rawPluginFile["keyFile-windows"].str;
739     }
740     catch(Exception e)
741     {
742         result.keyFileWindows = null;
743     }
744 
745     try
746     {
747         result.keyPasswordWindows = rawPluginFile["keyPassword-windows"].str;
748     }
749     catch(Exception e)
750     {
751         result.keyPasswordWindows = null;
752     }
753 
754     try
755     {
756         result.iconPathWindows = rawPluginFile["iconPath-windows"].str;
757     }
758     catch(Exception e)
759     {
760         info("Missing \"iconPath-windows\" in plugin.json (eg: \"gfx/myIcon.ico\")");
761     }
762 
763     try
764     {
765         result.iconPathOSX = rawPluginFile["iconPath-osx"].str;
766     }
767     catch(Exception e)
768     {
769         info("Missing \"iconPath-osx\" in plugin.json (eg: \"gfx/myIcon.png\")");
770     }
771 
772     // Mandatory keys, but with workarounds
773 
774     try
775     {
776         result.CFBundleIdentifierPrefix = rawPluginFile["CFBundleIdentifierPrefix"].str;
777     }
778     catch(Exception e)
779     {
780         warning("Missing \"CFBundleIdentifierPrefix\" in plugin.json (eg: \"com.myaudiocompany\")\n         => Using \"com.totoaudio\" instead.");
781         result.CFBundleIdentifierPrefix = "com.totoaudio";
782     }
783 
784     try
785     {
786         result.vendorName = rawPluginFile["vendorName"].str;
787 
788     }
789     catch(Exception e)
790     {
791         warning("Missing \"vendorName\" in plugin.json (eg: \"Example Corp\")\n         => Using \"Toto Audio\" instead.");
792         result.vendorName = "Toto Audio";
793     }
794 
795     try
796     {
797         result.vendorUniqueID = rawPluginFile["vendorUniqueID"].str;
798     }
799     catch(Exception e)
800     {
801         warning("Missing \"vendorUniqueID\" in plugin.json (eg: \"aucd\")\n         => Using \"Toto\" instead.");
802         result.vendorUniqueID = "Toto";
803     }
804 
805     if (result.vendorUniqueID.length != 4)
806         throw new Exception("\"vendorUniqueID\" should be a string of 4 characters (eg: \"aucd\")");
807 
808     if (!any!isUpper(result.vendorUniqueID))
809         throw new Exception("\"vendorUniqueID\" should contain at least one upper case character (eg: \"Aucd\")");
810 
811     try
812     {
813         result.pluginUniqueID = rawPluginFile["pluginUniqueID"].str;
814     }
815     catch(Exception e)
816     {
817         warning("Missing \"pluginUniqueID\" provided in plugin.json (eg: \"val8\")\n         => Using \"tot0\" instead, change it for a proper release.");
818         result.pluginUniqueID = "tot0";
819     }
820 
821     if (result.pluginUniqueID.length != 4)
822         throw new Exception("\"pluginUniqueID\" should be a string of 4 characters (eg: \"val8\")");
823 
824     // TODO: check for special characters in pluginUniqueID and vendorUniqueID
825     //       I'm not sure if Audio Unit would take anything not printable, would auval support it?
826 
827     // In developement, publicVersion should stay at 0.x.y to avoid various AU caches
828     // (this is only the theory...)
829     string publicV;
830     try
831     {
832         publicV = rawPluginFile["publicVersion"].str;
833     }
834     catch(Exception e)
835     {
836         warning("no \"publicVersion\" provided in plugin.json (eg: \"1.0.1\")\n         => Using \"0.0.0\" instead.");
837         publicV = "0.0.0";
838     }
839 
840     if (auto captures = matchFirst(publicV, regex(`(\d+)\.(\d+)\.(\d+)`)))
841     {
842         result.publicVersionMajor = to!int(captures[1]);
843         result.publicVersionMinor = to!int(captures[2]);
844         result.publicVersionPatch = to!int(captures[3]);
845     }
846     else
847     {
848         throw new Exception("\"publicVersion\" should follow the form x.y.z with 3 integers (eg: \"1.0.0\")");
849     }
850 
851     bool toBoolean(JSONValue value)
852     {
853         if (value.type == jsonTrue)
854             return true;
855         if (value.type == jsonFalse)
856             return false;
857         throw new Exception("Expected a boolean");
858     }
859 
860     try
861     {
862         result.isSynth = toBoolean(rawPluginFile["isSynth"]);
863     }
864     catch(Exception e)
865     {
866         warning("no \"isSynth\" provided in plugin.json (eg: \"true\")\n         => Using \"false\" instead.");
867         result.isSynth = false;
868     }
869 
870     try
871     {
872         result.receivesMIDI = toBoolean(rawPluginFile["receivesMIDI"]);
873     }
874     catch(Exception e)
875     {
876         warning("no \"receivesMIDI\" provided in plugin.json (eg: \"true\")\n         => Using \"false\" instead.");
877         result.receivesMIDI = false;
878     }
879 
880 
881     try
882     {
883         result.category = parsePluginCategory(rawPluginFile["category"].str);
884         if (result.category == PluginCategory.invalid)
885             throw new Exception("");
886     }
887     catch(Exception e)
888     {
889         error("Missing or invalid \"category\" provided in plugin.json (eg: \"effectDelay\")");
890         throw new Exception("=> Check dplug/client/daw.d to find a suitable \"category\" for plugin.json.");
891     }
892 
893     try
894     {
895         result.vendorAppleID = rawPluginFile["vendorAppleID"].str;
896     }
897     catch(Exception e){}
898     try
899     {
900         result.appSpecificPassword_altool = expandDplugVariables( rawPluginFile["appSpecificPassword-altool"].str );
901     }
902     catch(Exception e){}
903     try
904     {
905         result.appSpecificPassword_stapler = expandDplugVariables( rawPluginFile["appSpecificPassword-stapler"].str );
906     }
907     catch(Exception e){}
908 
909     try
910     {
911         result.pluginHomepage = rawPluginFile["pluginHomepage"].str;
912     }
913     catch(Exception e)
914     {
915         // Only warn on VST3 build if pluginHomepage is missing
916         result.pluginHomepage = null;
917     }
918 
919     try
920     {
921         result.vendorSupportEmail = rawPluginFile["vendorSupportEmail"].str;
922     }
923     catch(Exception e)
924     {
925         // Only warn on VST3 build if vendorSupportEmail is missing
926         result.vendorSupportEmail = null;
927     }
928 
929     try
930     {
931         result.iLokAccount = rawPluginFile["iLokAccount"].str;
932     }
933     catch(Exception e)
934     {
935         result.iLokAccount = null;
936     }
937 
938     try
939     {
940         result.iLokPassword = rawPluginFile["iLokPassword"].str;
941     }
942     catch(Exception e)
943     {
944         result.iLokPassword = null;
945     }
946 
947     try
948     {
949         result.wrapConfigGUID = rawPluginFile["wrapConfigGUID"].str;
950     }
951     catch(Exception e)
952     {
953         result.wrapConfigGUID = null;
954     }
955 
956     return result;
957 }
958 
959 bool toBool(JSONValue v)
960 {
961     if (v.type == jsonFalse)
962         return false;
963     else if (v.type == jsonTrue)
964         return true;
965     else
966         throw new Exception("expected boolean value");
967 }
968 
969 
970 string makePListFile(Plugin plugin, string config, bool hasIcon, bool isAudioComponentAPIImplemented)
971 {
972     string productVersion = plugin.publicVersionString;
973     string content = "";
974 
975     content ~= `<?xml version="1.0" encoding="UTF-8"?>` ~ "\n";
976     content ~= `<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">` ~ "\n";
977     content ~= `<plist version="1.0">` ~ "\n";
978     content ~= `    <dict>` ~ "\n";
979 
980     void addKeyString(string key, string value)
981     {
982         content ~= format("        <key>%s</key>\n        <string>%s</string>\n", key, value);
983     }
984 
985     addKeyString("CFBundleDevelopmentRegion", "English");
986 
987     addKeyString("CFBundleGetInfoString", productVersion ~ ", " ~ plugin.copyright);
988 
989     string CFBundleIdentifier;
990     if (configIsVST2(config))
991         CFBundleIdentifier = plugin.getVSTBundleIdentifier();
992     else if (configIsVST3(config))
993         CFBundleIdentifier = plugin.getVST3BundleIdentifier();
994     else if (configIsAU(config))
995         CFBundleIdentifier = plugin.getAUBundleIdentifier();
996     else if (configIsAAX(config))
997         CFBundleIdentifier = plugin.getAAXBundleIdentifier();
998     else if (configIsLV2(config))
999         CFBundleIdentifier = plugin.getLV2BundleIdentifier();
1000     else
1001         throw new Exception("Configuration name given by --config must start with \"VST\", \"VST3\", \"AU\", \"AAX\", or \"LV2\"");
1002 
1003     // Doesn't seem useful at all
1004     //addKeyString("CFBundleName", plugin.prettyName);
1005     //addKeyString("CFBundleExecutable", plugin.prettyName);
1006 
1007     addKeyString("CFBundleIdentifier", CFBundleIdentifier);
1008 
1009     addKeyString("CFBundleVersion", productVersion);
1010     addKeyString("CFBundleShortVersionString", productVersion);
1011 
1012     // PACE signing need this on Mac to find the executable to sign
1013     addKeyString("CFBundleExecutable", plugin.prettyName);
1014 
1015     if (isAudioComponentAPIImplemented && configIsAU(config))
1016     {
1017         content ~= "        <key>AudioComponents</key>\n";
1018         content ~= "        <array>\n";
1019         content ~= "            <dict>\n";
1020         content ~= "                <key>type</key>\n";
1021         if (plugin.isSynth)
1022             content ~= "                <string>aumu</string>\n";
1023         else if (plugin.receivesMIDI)
1024             content ~= "                <string>aumf</string>\n";
1025         else
1026             content ~= "                <string>aufx</string>\n";
1027 
1028         // We use VST unique plugin ID as subtype in Audio Unit
1029         // So apparently no chance to give a categoy here.
1030         char[4] uid = plugin.pluginUniqueID;
1031         string suid = escapeXMLString(uid.idup);//format("%c%c%c%c", uid[0], uid[1], uid[2], uid[3]));
1032         content ~= "                <key>subtype</key>\n";
1033         content ~= "                <string>" ~ suid ~ "</string>\n";
1034 
1035         char[4] vid = plugin.vendorUniqueID;
1036         string svid = escapeXMLString(vid.idup);
1037         content ~= "                <key>manufacturer</key>\n";
1038         content ~= "                <string>" ~ svid ~ "</string>\n";
1039         content ~= "                <key>name</key>\n";
1040         content ~= format("                <string>%s</string>\n", escapeXMLString(plugin.vendorName ~ ": " ~ plugin.pluginName));
1041         content ~= "                <key>description</key>\n";
1042         content ~= format("                <string>%s</string>\n", escapeXMLString(plugin.vendorName ~ " " ~ plugin.pluginName));
1043         content ~= "                <key>version</key>\n";
1044         content ~= format("                <integer>%s</integer>\n", plugin.publicVersionInt()); // TODO correct?
1045         content ~= "                <key>factoryFunction</key>\n";
1046         content ~= "                <string>dplugAUComponentFactoryFunction</string>\n";
1047         content ~= "                <key>sandboxSafe</key>\n";
1048         content ~= "                <true/>\n";
1049         content ~= "            </dict>\n";
1050         content ~= "        </array>\n";
1051     }
1052 
1053     addKeyString("CFBundleInfoDictionaryVersion", "6.0");
1054     addKeyString("CFBundlePackageType", "BNDL");
1055     addKeyString("CFBundleSignature", plugin.pluginUniqueID); // doesn't matter http://stackoverflow.com/questions/1875912/naming-convention-for-cfbundlesignature-and-cfbundleidentifier
1056 
1057    // Set to 10.9
1058     addKeyString("LSMinimumSystemVersion", "10.9.0");
1059 
1060    // content ~= "        <key>VSTWindowCompositing</key><true/>\n";
1061 
1062     if (hasIcon)
1063         addKeyString("CFBundleIconFile", "icon");
1064     content ~= `    </dict>` ~ "\n";
1065     content ~= `</plist>` ~ "\n";
1066     return content;
1067 }
1068 
1069 // return path of newly made icon
1070 string makeMacIcon(string outputDir, string pluginName, string pngPath)
1071 {
1072     string iconSetDir = buildPath(outputDir, "temp/" ~ pluginName ~ ".iconset");
1073     string outputIcon = buildPath(outputDir, "temp/" ~ pluginName ~ ".icns");
1074 
1075     if(!outputIcon.exists)
1076     {
1077         //string cmd = format("lipo -create %s %s -output %s", path32, path64, exePath);
1078         try
1079         {
1080             safeCommand(format("mkdir %s", iconSetDir));
1081         }
1082         catch(Exception e)
1083         {
1084             cwritefln(" => %s".yellow, e.msg);
1085         }
1086         safeCommand(format("sips -z 16 16     %s --out %s/icon_16x16.png", pngPath, iconSetDir));
1087         safeCommand(format("sips -z 32 32     %s --out %s/icon_16x16@2x.png", pngPath, iconSetDir));
1088         safeCommand(format("sips -z 32 32     %s --out %s/icon_32x32.png", pngPath, iconSetDir));
1089         safeCommand(format("sips -z 64 64     %s --out %s/icon_32x32@2x.png", pngPath, iconSetDir));
1090         safeCommand(format("sips -z 128 128   %s --out %s/icon_128x128.png", pngPath, iconSetDir));
1091         safeCommand(format("sips -z 256 256   %s --out %s/icon_128x128@2x.png", pngPath, iconSetDir));
1092         safeCommand(format("iconutil --convert icns --output %s %s", outputIcon, iconSetDir));
1093     }
1094     return outputIcon;
1095 }
1096 
1097 string makeRSRC_internal(string outputDir, Plugin plugin, Arch arch, bool verbose)
1098 {
1099     if (arch != Arch.x86_64 
1100         && arch != Arch.arm64 
1101         && arch != Arch.universalBinary)
1102     {
1103         throw new Exception("Can't use internal .rsrc generation for this arch");
1104     }
1105 
1106     cwritefln("*** Generating a .rsrc file for the bundle...".white);
1107 
1108     string rsrcPath = outputDir ~ "/temp/plugin-" ~ convertArchToPrettyString(arch) ~ ".rsrc";
1109     RSRCWriter rsrc;
1110     rsrc.addType("STR ");
1111     rsrc.addType("dlle");
1112     rsrc.addType("thng");
1113     rsrc.addResource(0, 1000, true, null, makeRSRC_pstring(plugin.vendorName ~ ": " ~ plugin.pluginName));
1114     rsrc.addResource(0, 1001, true, null, makeRSRC_pstring(plugin.pluginName ~ " AU"));
1115     rsrc.addResource(1, 1000, false, null, makeRSRC_cstring("dplugAUEntryPoint"));
1116     ubyte[] thng;
1117     {
1118         if (plugin.isSynth)
1119             thng ~= makeRSRC_fourCC("aumu");
1120         else if (plugin.receivesMIDI)
1121             thng ~= makeRSRC_fourCC("aumf");
1122         else
1123             thng ~= makeRSRC_fourCC("aufx");
1124         thng ~= makeRSRC_fourCC_string(plugin.pluginUniqueID);
1125         thng ~= makeRSRC_fourCC_string(plugin.vendorUniqueID);
1126         thng.writeBE_uint(0);
1127         thng.writeBE_uint(0);
1128         thng.writeBE_uint(0);
1129         thng.writeBE_ushort(0);
1130         thng ~= makeRSRC_fourCC("STR ");
1131         thng.writeBE_ushort(1000);
1132         thng ~= makeRSRC_fourCC("STR ");
1133         thng.writeBE_ushort(1001);
1134         thng.writeBE_uint(0); // icon
1135         thng.writeBE_ushort(0);
1136         thng.writeBE_uint(plugin.publicVersionInt());
1137         enum componentDoAutoVersion = 0x01;
1138         enum componentHasMultiplePlatforms = 0x08;
1139         thng.writeBE_uint(componentDoAutoVersion | componentHasMultiplePlatforms);
1140         thng.writeBE_ushort(0);
1141 
1142         if (arch == Arch.x86_64)
1143         {
1144             thng.writeBE_uint(1); // 1 platform
1145             thng.writeBE_uint(0x10000000);
1146             thng ~= makeRSRC_fourCC("dlle");
1147             thng.writeBE_ushort(1000);
1148             thng.writeBE_ushort(8 /* platformX86_64NativeEntryPoint */);
1149         }
1150         else if (arch == Arch.arm64)
1151         {
1152             thng.writeBE_uint(1); // 1 platform
1153             thng.writeBE_uint(0x10000000);
1154             thng ~= makeRSRC_fourCC("dlle");
1155             thng.writeBE_ushort(1000);
1156             thng.writeBE_ushort(9 /* platformArm64NativeEntryPoint */);
1157         }
1158         else if (arch == Arch.universalBinary)
1159         {
1160             thng.writeBE_uint(2); // 2 platform, arm64 then x86_64
1161 
1162             thng.writeBE_uint(0x10000000);
1163             thng ~= makeRSRC_fourCC("dlle");
1164             thng.writeBE_ushort(1000);
1165             thng.writeBE_ushort(9 /* platformArm64NativeEntryPoint */);
1166             
1167             thng.writeBE_uint(0x10000000);
1168             thng ~= makeRSRC_fourCC("dlle");
1169             thng.writeBE_ushort(1000);
1170             thng.writeBE_ushort(8 /* platformX86_64NativeEntryPoint */);
1171         }
1172         else
1173             assert(false, "not supported yet");
1174     }
1175 
1176     rsrc.addResource(2, 1000, false, plugin.vendorName ~ ": " ~ plugin.pluginName, thng);
1177 
1178     std.file.write(rsrcPath, rsrc.write());
1179     cwritefln("    => Written %s bytes.".green, getSize(rsrcPath));
1180     cwriteln();
1181     return rsrcPath;
1182 }
1183 
1184 string makeRSRC_with_Rez(Plugin plugin, Arch arch, bool verbose)
1185 {
1186     if (arch != Arch.x86_64)
1187         throw new Exception("Can't use --rez for another arch than x86_64");
1188     string pluginName = plugin.pluginName;
1189     cwritefln("*** Generating a .rsrc file for the bundle, using Rez...".white);
1190     string temp = tempDir();
1191 
1192     string rPath = buildPath(temp, "plugin.r");
1193 
1194     File rFile = File(rPath, "w");
1195     static immutable string rFileBase = cast(string) import("plugin-base.r");
1196 
1197     rFile.writefln(`#define PLUG_MFR "%s"`, plugin.vendorName); // no C escaping there, FUTURE
1198     rFile.writefln("#define PLUG_MFR_ID '%s'", plugin.vendorUniqueID);
1199     rFile.writefln(`#define PLUG_NAME "%s"`, pluginName); // no C escaping there, FUTURE
1200     rFile.writefln("#define PLUG_UNIQUE_ID '%s'", plugin.pluginUniqueID);
1201     rFile.writefln("#define PLUG_VER %d", plugin.publicVersionInt());
1202 
1203     rFile.writefln("#define PLUG_IS_INST %s", (plugin.isSynth ? "1" : "0"));
1204     rFile.writefln("#define PLUG_DOES_MIDI %s", (plugin.receivesMIDI ? "1" : "0"));
1205 
1206     rFile.writeln(rFileBase);
1207     rFile.close();
1208 
1209     string rsrcPath = "reference.rsrc";
1210 
1211     string archFlags;
1212     final switch(arch) with (Arch)
1213     {
1214         case x86: archFlags = "-arch i386"; break;
1215         case x86_64: archFlags = "-arch x86_64"; break;
1216         case arm32: assert(false);
1217         case arm64: assert(false);
1218         case universalBinary: assert(false);
1219     }
1220 
1221     string verboseFlag = verbose ? " -p" : "";
1222 
1223     safeCommand(format("rez %s%s -o %s -useDF %s", archFlags, verboseFlag, rsrcPath, rPath));
1224 
1225 
1226     if (!exists(rsrcPath))
1227         throw new Exception(format("%s wasn't created", rsrcPath));
1228 
1229     if (getSize(rsrcPath) == 0)
1230         throw new Exception(format("%s is an empty file", rsrcPath));
1231 
1232     cwritefln("    => Written %s bytes.".green, getSize(rsrcPath));
1233     cwriteln();
1234     return rsrcPath;
1235 }