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