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 }