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