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