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