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(" => %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(" => 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(" => Written %s bytes.".lgreen, getSize(rsrcPath)); 1411 cwriteln(); 1412 return rsrcPath; 1413 }