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