1 module workspaced.com.dub; 2 3 import core.exception; 4 import core.sync.mutex; 5 import core.thread; 6 7 import std.algorithm; 8 import std.array : appender; 9 import std.conv; 10 import std.exception; 11 import std.json : JSONType, JSONValue; 12 import std.parallelism; 13 import std.regex; 14 import std.stdio; 15 import std..string; 16 17 import painlessjson : toJSON, fromJSON; 18 19 import workspaced.api; 20 21 import dub.description; 22 import dub.dub; 23 import dub.package_; 24 import dub.project; 25 26 import dub.compilers.buildsettings; 27 import dub.compilers.compiler; 28 import dub.generators.build; 29 import dub.generators.generator; 30 31 import dub.internal.vibecompat.core.log; 32 import dub.internal.vibecompat.inet.url; 33 34 import dub.recipe.io; 35 36 @component("dub") 37 class DubComponent : ComponentWrapper 38 { 39 mixin DefaultComponentWrapper; 40 41 static void registered() 42 { 43 setLogLevel(LogLevel.none); 44 } 45 46 protected void load() 47 { 48 if (!refInstance) 49 throw new Exception("dub requires to be instanced"); 50 51 if (config.get!bool("dub", "registerImportProvider", true)) 52 importPathProvider = &imports; 53 if (config.get!bool("dub", "registerStringImportProvider", true)) 54 stringImportPathProvider = &stringImports; 55 if (config.get!bool("dub", "registerImportFilesProvider", false)) 56 importFilesProvider = &fileImports; 57 if (config.get!bool("dub", "registerProjectVersionsProvider", true)) 58 projectVersionsProvider = &versions; 59 if (config.get!bool("dub", "registerDebugSpecificationsProvider", true)) 60 debugSpecificationsProvider = &debugVersions; 61 62 try 63 { 64 start(); 65 66 _configuration = _dub.project.getDefaultConfiguration(_platform); 67 if (!_dub.project.configurations.canFind(_configuration)) 68 { 69 stderr.writeln("Dub Error: No configuration available"); 70 workspaced.broadcast(refInstance, JSONValue([ 71 "type": JSONValue("warning"), 72 "component": JSONValue("dub"), 73 "detail": JSONValue("invalid-default-config") 74 ])); 75 } 76 else 77 updateImportPaths(false); 78 } 79 catch (Exception e) 80 { 81 if (!_dub || !_dub.project) 82 throw e; 83 stderr.writeln("Dub Error (ignored): ", e); 84 } 85 /*catch (AssertError e) 86 { 87 if (!_dub || !_dub.project) 88 throw e; 89 stderr.writeln("Dub Error (ignored): ", e); 90 }*/ 91 } 92 93 private void start() 94 { 95 _dubRunning = false; 96 _dub = new Dub(instance.cwd, null, SkipPackageSuppliers.none); 97 _dub.packageManager.getOrLoadPackage(NativePath(instance.cwd)); 98 _dub.loadPackage(); 99 _dub.project.validate(); 100 101 // mark all packages as optional so we don't crash 102 int missingPackages; 103 auto optionalified = optionalifyPackages; 104 foreach (ref pkg; _dub.project.getTopologicalPackageList()) 105 { 106 optionalifyRecipe(pkg); 107 foreach (dep; pkg.getAllDependencies().filter!(a => optionalified.canFind(a.name))) 108 { 109 auto d = _dub.project.getDependency(dep.name, true); 110 if (!d) 111 missingPackages++; 112 else 113 optionalifyRecipe(d); 114 } 115 } 116 117 if (!_compilerBinaryName.length) 118 _compilerBinaryName = _dub.defaultCompiler; 119 setCompiler(_compilerBinaryName); 120 121 _settingsTemplate = cast() _dub.project.rootPackage.getBuildSettings(); 122 123 if (missingPackages > 0) 124 { 125 upgrade(false); 126 optionalifyPackages(); 127 } 128 129 _dubRunning = true; 130 } 131 132 private string[] optionalifyPackages() 133 { 134 bool[Package] visited; 135 string[] optionalified; 136 foreach (pkg; _dub.project.dependencies) 137 optionalified ~= optionalifyRecipe(cast() pkg); 138 return optionalified; 139 } 140 141 private string[] optionalifyRecipe(Package pkg) 142 { 143 string[] optionalified; 144 foreach (key, ref value; pkg.recipe.buildSettings.dependencies) 145 { 146 if (!value.optional) 147 { 148 value.optional = true; 149 value.default_ = true; 150 optionalified ~= key; 151 } 152 } 153 foreach (ref config; pkg.recipe.configurations) 154 foreach (key, ref value; config.buildSettings.dependencies) 155 { 156 if (!value.optional) 157 { 158 value.optional = true; 159 value.default_ = true; 160 optionalified ~= key; 161 } 162 } 163 return optionalified; 164 } 165 166 private void restart() 167 { 168 _dub.destroy(); 169 _dubRunning = false; 170 start(); 171 } 172 173 bool isRunning() 174 { 175 return _dub !is null && _dub.project !is null && _dub.project.rootPackage !is null 176 && _dubRunning; 177 } 178 179 /// Reloads the dub.json or dub.sdl file from the cwd 180 /// Returns: `false` if there are no import paths available 181 Future!bool update() 182 { 183 restart(); 184 mixin(gthreadsAsyncProxy!`updateImportPaths(false)`); 185 } 186 187 bool updateImportPaths(bool restartDub = true) 188 { 189 validateConfiguration(); 190 191 if (restartDub) 192 restart(); 193 194 GeneratorSettings settings; 195 settings.platform = _platform; 196 settings.config = _configuration; 197 settings.buildType = _buildType; 198 settings.compiler = _compiler; 199 settings.buildSettings = _settings; 200 settings.buildSettings.addOptions(BuildOption.syntaxOnly); 201 settings.combined = true; 202 settings.run = false; 203 204 try 205 { 206 auto paths = _dub.project.listBuildSettings(settings, [ 207 "import-paths", "string-import-paths", "source-files", "versions", "debug-versions" 208 ], ListBuildSettingsFormat.listNul); 209 _importPaths = paths[0].split('\0'); 210 _stringImportPaths = paths[1].split('\0'); 211 _importFiles = paths[2].split('\0'); 212 _versions = paths[3].split('\0'); 213 _debugVersions = paths[4].split('\0'); 214 return _importPaths.length > 0 || _importFiles.length > 0; 215 } 216 catch (Exception e) 217 { 218 workspaced.broadcast(refInstance, JSONValue([ 219 "type": JSONValue("error"), 220 "component": JSONValue("dub"), 221 "detail": JSONValue("Error while listing import paths: " ~ e.toString) 222 ])); 223 _importPaths = []; 224 _stringImportPaths = []; 225 return false; 226 } 227 } 228 229 /// Calls `dub upgrade` 230 void upgrade(bool save = true) 231 { 232 if (save) 233 _dub.upgrade(UpgradeOptions.select | UpgradeOptions.upgrade); 234 else 235 _dub.upgrade(UpgradeOptions.noSaveSelections); 236 } 237 238 /// Throws if configuration is invalid, otherwise does nothing. 239 void validateConfiguration() const 240 { 241 if (!_dub.project.configurations.canFind(_configuration)) 242 throw new Exception("Cannot use dub with invalid configuration"); 243 } 244 245 /// Throws if configuration is invalid or targetType is none or source library, otherwise does nothing. 246 void validateBuildConfiguration() 247 { 248 if (!_dub.project.configurations.canFind(_configuration)) 249 throw new Exception("Cannot use dub with invalid configuration"); 250 if (_settings.targetType == TargetType.none) 251 throw new Exception("Cannot build with dub with targetType == none"); 252 if (_settings.targetType == TargetType.sourceLibrary) 253 throw new Exception("Cannot build with dub with targetType == sourceLibrary"); 254 } 255 256 /// Lists all dependencies. This will go through all dependencies and contain the dependencies of dependencies. You need to create a tree structure from this yourself. 257 /// Returns: `[{dependencies: string[string], ver: string, name: string}]` 258 auto dependencies() @property const 259 { 260 validateConfiguration(); 261 262 return listDependencies(_dub.project); 263 } 264 265 /// Lists dependencies of the root package. This can be used as a base to create a tree structure. 266 string[] rootDependencies() @property const 267 { 268 validateConfiguration(); 269 270 return listDependencies(_dub.project.rootPackage); 271 } 272 273 /// Returns the path to the root package recipe (dub.json/dub.sdl) 274 /// 275 /// Note that this can be empty if the package is not in the local file system. 276 string recipePath() @property 277 { 278 return _dub.project.rootPackage.recipePath.toString; 279 } 280 281 /// Re-parses the package recipe on the file system and returns if the syntax is valid. 282 /// Returns: empty string/null if no error occured, error message if an error occured. 283 string validateRecipeSyntaxOnFileSystem() 284 { 285 auto p = recipePath; 286 if (!p.length) 287 return "Package is not in local file system"; 288 289 try 290 { 291 readPackageRecipe(p); 292 return null; 293 } 294 catch (Exception e) 295 { 296 return e.msg; 297 } 298 } 299 300 /// Lists all import paths 301 string[] imports() @property nothrow 302 { 303 return _importPaths; 304 } 305 306 /// Lists all string import paths 307 string[] stringImports() @property nothrow 308 { 309 return _stringImportPaths; 310 } 311 312 /// Lists all import paths to files 313 string[] fileImports() @property nothrow 314 { 315 return _importFiles; 316 } 317 318 /// Lists the currently defined versions 319 string[] versions() @property nothrow 320 { 321 return _versions; 322 } 323 324 /// Lists the currently defined debug versions (debug specifications) 325 string[] debugVersions() @property nothrow 326 { 327 return _debugVersions; 328 } 329 330 /// Lists all configurations defined in the package description 331 string[] configurations() @property 332 { 333 return _dub.project.configurations; 334 } 335 336 PackageBuildSettings rootPackageBuildSettings() @property 337 { 338 auto pkg = _dub.project.rootPackage; 339 BuildSettings settings = pkg.getBuildSettings(_platform, _configuration); 340 return PackageBuildSettings(settings, 341 pkg.path.toString, 342 pkg.name, 343 _dub.project.rootPackage.recipePath.toNativeString()); 344 } 345 346 /// Lists all build types defined in the package description AND the predefined ones from dub ("plain", "debug", "release", "release-debug", "release-nobounds", "unittest", "docs", "ddox", "profile", "profile-gc", "cov", "unittest-cov") 347 string[] buildTypes() @property 348 { 349 string[] types = [ 350 "plain", "debug", "release", "release-debug", "release-nobounds", 351 "unittest", "docs", "ddox", "profile", "profile-gc", "cov", "unittest-cov" 352 ]; 353 foreach (type, info; _dub.project.rootPackage.recipe.buildTypes) 354 types ~= type; 355 return types; 356 } 357 358 /// Gets the current selected configuration 359 string configuration() @property 360 { 361 return _configuration; 362 } 363 364 /// Selects a new configuration and updates the import paths accordingly 365 /// Returns: `false` if there are no import paths in the new configuration 366 bool setConfiguration(string configuration) 367 { 368 if (!_dub.project.configurations.canFind(configuration)) 369 return false; 370 _configuration = configuration; 371 _settingsTemplate = cast() _dub.project.rootPackage.getBuildSettings(configuration); 372 return updateImportPaths(false); 373 } 374 375 /// List all possible arch types for current set compiler 376 string[] archTypes() @property 377 { 378 string[] types = ["x86_64", "x86"]; 379 380 string compilerName = _compiler.name; 381 382 version (Windows) 383 { 384 if (compilerName == "dmd") 385 { 386 types ~= "x86_mscoff"; 387 } 388 } 389 if (compilerName == "gdc") 390 { 391 types ~= ["arm", "arm_thumb"]; 392 } 393 394 return types; 395 } 396 397 /// Returns the current selected arch type 398 string archType() @property 399 { 400 return _archType; 401 } 402 403 /// Selects a new arch type and updates the import paths accordingly 404 /// Returns: `false` if there are no import paths in the new arch type 405 bool setArchType(JSONValue request) 406 { 407 enforce(request.type == JSONType.object && "arch-type" in request, "arch-type not in request"); 408 auto type = request["arch-type"].fromJSON!string; 409 if (archTypes.canFind(type)) 410 { 411 _archType = type; 412 return updateImportPaths(false); 413 } 414 else 415 { 416 return false; 417 } 418 } 419 420 /// Returns the current selected build type 421 string buildType() @property 422 { 423 return _buildType; 424 } 425 426 /// Selects a new build type and updates the import paths accordingly 427 /// Returns: `false` if there are no import paths in the new build type 428 bool setBuildType(JSONValue request) 429 { 430 enforce(request.type == JSONType.object && "build-type" in request, "build-type not in request"); 431 auto type = request["build-type"].fromJSON!string; 432 if (buildTypes.canFind(type)) 433 { 434 _buildType = type; 435 return updateImportPaths(false); 436 } 437 else 438 { 439 return false; 440 } 441 } 442 443 /// Returns the current selected compiler 444 string compiler() @property 445 { 446 return _compilerBinaryName; 447 } 448 449 /// Selects a new compiler for building 450 /// Returns: `false` if the compiler does not exist 451 bool setCompiler(string compiler) 452 { 453 try 454 { 455 _compilerBinaryName = compiler; 456 _compiler = getCompiler(compiler); // make sure it gets a valid compiler 457 } 458 catch (Exception e) 459 { 460 return false; 461 } 462 _platform = _compiler.determinePlatform(_settings, _compilerBinaryName, _archType); 463 _settingsTemplate.getPlatformSettings(_settings, _platform, _dub.project.rootPackage.path); 464 return _compiler !is null; 465 } 466 467 /// Returns the project name 468 string name() @property 469 { 470 return _dub.projectName; 471 } 472 473 /// Returns the project path 474 auto path() @property 475 { 476 return _dub.projectPath; 477 } 478 479 /// Returns whether there is a target set to build. If this is false then build will throw an exception. 480 bool canBuild() @property 481 { 482 if (_settings.targetType == TargetType.none || _settings.targetType == TargetType.sourceLibrary 483 || !_dub.project.configurations.canFind(_configuration)) 484 return false; 485 return true; 486 } 487 488 /// Asynchroniously builds the project WITHOUT OUTPUT. This is intended for linting code and showing build errors quickly inside the IDE. 489 Future!(BuildIssue[]) build() 490 { 491 import std.process : thisProcessID; 492 import std.file : tempDir; 493 import std.random : uniform; 494 495 validateBuildConfiguration(); 496 497 // copy to this thread 498 auto compiler = _compiler; 499 auto buildPlatform = _platform; 500 501 GeneratorSettings settings; 502 settings.platform = buildPlatform; 503 settings.config = _configuration; 504 settings.buildType = _buildType; 505 settings.compiler = compiler; 506 settings.buildSettings = _settings; 507 508 auto ret = new typeof(return); 509 new Thread({ 510 try 511 { 512 auto issues = appender!(BuildIssue[]); 513 514 settings.compileCallback = (status, output) { 515 string[] lines = output.splitLines; 516 foreach (line; lines) 517 { 518 auto match = line.matchFirst(errorFormat); 519 if (match) 520 { 521 issues ~= BuildIssue(match[2].to!int, match[3].toOr!int(0), 522 match[1], match[4].to!ErrorType, match[5]); 523 } 524 else 525 { 526 auto contMatch = line.matchFirst(errorFormatCont); 527 if (issues.data.length && contMatch) 528 { 529 issues ~= BuildIssue(contMatch[2].to!int, 530 contMatch[3].toOr!int(1), contMatch[1], 531 issues.data[$ - 1].type, contMatch[4], true); 532 } 533 else if (line.canFind("is deprecated")) 534 { 535 auto deprMatch = line.matchFirst(deprecationFormat); 536 if (deprMatch) 537 { 538 issues ~= BuildIssue(deprMatch[2].to!int, deprMatch[3].toOr!int(1), 539 deprMatch[1], ErrorType.Deprecation, 540 deprMatch[4] ~ " is deprecated, use " ~ deprMatch[5] ~ " instead."); 541 } 542 } 543 } 544 } 545 }; 546 try 547 { 548 import workspaced.dub.lintgenerator : DubLintGenerator; 549 550 new DubLintGenerator(_dub.project).generate(settings); 551 } 552 catch (Exception e) 553 { 554 if (!e.msg.matchFirst(harmlessExceptionFormat)) 555 throw e; 556 } 557 ret.finish(issues.data); 558 } 559 catch (Throwable t) 560 { 561 ret.error(t); 562 } 563 }).start(); 564 return ret; 565 } 566 567 /// Converts the root package recipe to another format. 568 /// Params: 569 /// format = either "json" or "sdl". 570 string convertRecipe(string format) 571 { 572 import dub.recipe.io : serializePackageRecipe; 573 import std.array : appender; 574 575 auto dst = appender!string; 576 serializePackageRecipe(dst, _dub.project.rootPackage.rawRecipe, "dub." ~ format); 577 return dst.data; 578 } 579 580 /// Tries to find a suitable code byte range where a given dub build issue 581 /// applies to. 582 /// Returns: `[pos, pos]` if not found, otherwise range in bytes which might 583 /// not contain the position at all. 584 int[2] resolveDiagnosticRange(scope const(char)[] code, int position, 585 scope const(char)[] diagnostic) 586 { 587 import dparse.lexer : getTokensForParser, LexerConfig; 588 import dparse.parser : parseModule; 589 import dparse.rollback_allocator : RollbackAllocator; 590 import workspaced.dub.diagnostics : resolveDubDiagnosticRange; 591 592 LexerConfig config; 593 RollbackAllocator rba; 594 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 595 auto parsed = parseModule(tokens, "equal_finder.d", &rba); 596 597 return resolveDubDiagnosticRange(code, tokens, parsed, position, diagnostic); 598 } 599 600 private: 601 Dub _dub; 602 bool _dubRunning = false; 603 string _configuration; 604 string _archType = "x86_64"; 605 string _buildType = "debug"; 606 string _compilerBinaryName; 607 Compiler _compiler; 608 BuildSettingsTemplate _settingsTemplate; 609 BuildSettings _settings; 610 BuildPlatform _platform; 611 string[] _importPaths, _stringImportPaths, _importFiles, _versions, _debugVersions; 612 } 613 614 /// 615 enum ErrorType : ubyte 616 { 617 /// 618 Error = 0, 619 /// 620 Warning = 1, 621 /// 622 Deprecation = 2 623 } 624 625 /// Returned by build 626 struct BuildIssue 627 { 628 /// 629 int line, column; 630 /// 631 string file; 632 /// The error type (Error/Warning/Deprecation) outputted by dmd or inherited from the last error if this is additional information of the last issue. (indicated by cont) 633 ErrorType type; 634 /// 635 string text; 636 /// true if this is additional error information for the last error. 637 bool cont; 638 } 639 640 /// returned by rootPackageBuildSettings 641 struct PackageBuildSettings 642 { 643 /// construct from dub build settings 644 this(BuildSettings dubBuildSettings, string packagePath, string packageName, string recipePath) 645 { 646 foreach (i, ref val; this.tupleof[0 .. __IGNORE_TRAIL]) 647 { 648 enum name = __traits(identifier, this.tupleof[i]); 649 static if (__traits(hasMember, dubBuildSettings, name)) 650 val = __traits(getMember, dubBuildSettings, name); 651 } 652 this.packagePath = packagePath; 653 this.packageName = packageName; 654 this.recipePath = recipePath; 655 656 if (!targetName.length) 657 targetName = packageName; 658 659 version (Windows) 660 targetName ~= ".exe"; 661 662 this.targetType = dubBuildSettings.targetType.to!string; 663 foreach (enumMember; __traits(allMembers, BuildOption)) 664 { 665 enum value = __traits(getMember, BuildOption, enumMember); 666 if (value != 0 && dubBuildSettings.options.opDispatch!enumMember) 667 this.buildOptions ~= enumMember; 668 } 669 foreach (enumMember; __traits(allMembers, BuildRequirement)) 670 { 671 enum value = __traits(getMember, BuildRequirement, enumMember); 672 if (value != 0 && dubBuildSettings.requirements.opDispatch!enumMember) 673 this.buildRequirements ~= enumMember; 674 } 675 } 676 677 string packagePath; 678 string packageName; 679 string recipePath; 680 681 string targetPath; /// same as dub BuildSettings 682 string targetName; /// same as dub BuildSettings 683 string workingDirectory; /// same as dub BuildSettings 684 string mainSourceFile; /// same as dub BuildSettings 685 string[] dflags; /// same as dub BuildSettings 686 string[] lflags; /// same as dub BuildSettings 687 string[] libs; /// same as dub BuildSettings 688 string[] linkerFiles; /// same as dub BuildSettings 689 string[] sourceFiles; /// same as dub BuildSettings 690 string[] copyFiles; /// same as dub BuildSettings 691 string[] extraDependencyFiles; /// same as dub BuildSettings 692 string[] versions; /// same as dub BuildSettings 693 string[] debugVersions; /// same as dub BuildSettings 694 string[] versionFilters; /// same as dub BuildSettings 695 string[] debugVersionFilters; /// same as dub BuildSettings 696 string[] importPaths; /// same as dub BuildSettings 697 string[] stringImportPaths; /// same as dub BuildSettings 698 string[] importFiles; /// same as dub BuildSettings 699 string[] stringImportFiles; /// same as dub BuildSettings 700 string[] preGenerateCommands; /// same as dub BuildSettings 701 string[] postGenerateCommands; /// same as dub BuildSettings 702 string[] preBuildCommands; /// same as dub BuildSettings 703 string[] postBuildCommands; /// same as dub BuildSettings 704 string[] preRunCommands; /// same as dub BuildSettings 705 string[] postRunCommands; /// same as dub BuildSettings 706 707 private enum __IGNORE_TRAIL = 2; // number of ignored settings below this line 708 709 string targetType; /// same as dub BuildSettings 710 string[] buildOptions; /// same as dub BuildSettings 711 string[] buildRequirements; /// same as dub BuildSettings 712 } 713 714 private: 715 716 T toOr(T)(string s, T defaultValue) 717 { 718 if (!s || !s.length) 719 return defaultValue; 720 return s.to!T; 721 } 722 723 enum harmlessExceptionFormat = ctRegex!(`failed with exit code`, "g"); 724 enum errorFormat = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (Deprecation|Warning|Error): (.*)`, "gi"); 725 enum errorFormatCont = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\):[ ]{6,}(.*)`, "g"); 726 enum deprecationFormat = ctRegex!( 727 `(.*?)\((\d+)(?:,(\d+))?\): (.*?) is deprecated, use (.*?) instead.$`, "g"); 728 729 struct DubPackageInfo 730 { 731 string[string] dependencies; 732 string ver; 733 string name; 734 string path; 735 string description; 736 string homepage; 737 const(string)[] authors; 738 string copyright; 739 string license; 740 DubPackageInfo[] subPackages; 741 742 void fill(in PackageRecipe recipe) 743 { 744 description = recipe.description; 745 homepage = recipe.homepage; 746 authors = recipe.authors; 747 copyright = recipe.copyright; 748 license = recipe.license; 749 750 foreach (subpackage; recipe.subPackages) 751 { 752 DubPackageInfo info; 753 info.ver = subpackage.recipe.version_; 754 info.name = subpackage.recipe.name; 755 info.path = subpackage.path; 756 info.fill(subpackage.recipe); 757 } 758 } 759 } 760 761 DubPackageInfo getInfo(in Package dep) 762 { 763 DubPackageInfo info; 764 info.name = dep.name; 765 info.ver = dep.version_.toString; 766 info.path = dep.path.toString; 767 info.fill(dep.recipe); 768 foreach (subDep; dep.getAllDependencies()) 769 { 770 info.dependencies[subDep.name] = subDep.spec.toString; 771 } 772 return info; 773 } 774 775 auto listDependencies(scope const Project project) 776 { 777 auto deps = project.dependencies; 778 DubPackageInfo[] dependencies; 779 if (deps is null) 780 return dependencies; 781 foreach (dep; deps) 782 { 783 dependencies ~= getInfo(dep); 784 } 785 return dependencies; 786 } 787 788 string[] listDependencies(scope const Package pkg) 789 { 790 auto deps = pkg.getAllDependencies(); 791 string[] dependencies; 792 if (deps is null) 793 return dependencies; 794 foreach (dep; deps) 795 dependencies ~= dep.name; 796 return dependencies; 797 }