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