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