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() const @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() const @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 if (compilerName == "dmd") 383 { 384 // https://github.com/dlang/dub/blob/master/source/dub/compilers/dmd.d#L110 385 version (Windows) 386 { 387 types ~= ["x86_omf", "x86_mscoff"]; 388 } 389 } 390 else if (compilerName == "gdc") 391 { 392 // https://github.com/dlang/dub/blob/master/source/dub/compilers/gdc.d#L69 393 types ~= ["arm", "arm_thumb"]; 394 } 395 else if (compilerName == "ldc") 396 { 397 // https://github.com/dlang/dub/blob/master/source/dub/compilers/ldc.d#L80 398 types ~= ["aarch64", "powerpc64"]; 399 } 400 401 return types; 402 } 403 404 /// Returns the current selected arch type 405 string archType() @property 406 { 407 return _archType; 408 } 409 410 /// Selects a new arch type and updates the import paths accordingly 411 /// Returns: `false` if there are no import paths in the new arch type 412 bool setArchType(JSONValue request) 413 { 414 enforce(request.type == JSONType.object && "arch-type" in request, "arch-type not in request"); 415 auto type = request["arch-type"].fromJSON!string; 416 if (archTypes.canFind(type)) 417 { 418 _archType = type; 419 return updateImportPaths(false); 420 } 421 else 422 { 423 return false; 424 } 425 } 426 427 /// Returns the current selected build type 428 string buildType() @property 429 { 430 return _buildType; 431 } 432 433 /// Selects a new build type and updates the import paths accordingly 434 /// Returns: `false` if there are no import paths in the new build type 435 bool setBuildType(JSONValue request) 436 { 437 enforce(request.type == JSONType.object && "build-type" in request, "build-type not in request"); 438 auto type = request["build-type"].fromJSON!string; 439 if (buildTypes.canFind(type)) 440 { 441 _buildType = type; 442 return updateImportPaths(false); 443 } 444 else 445 { 446 return false; 447 } 448 } 449 450 /// Returns the current selected compiler 451 string compiler() const @property 452 { 453 return _compilerBinaryName; 454 } 455 456 /// Selects a new compiler for building 457 /// Returns: `false` if the compiler does not exist 458 bool setCompiler(string compiler) 459 { 460 try 461 { 462 _compilerBinaryName = compiler; 463 _compiler = getCompiler(compiler); // make sure it gets a valid compiler 464 } 465 catch (Exception e) 466 { 467 return false; 468 } 469 _platform = _compiler.determinePlatform(_settings, _compilerBinaryName, _archType); 470 _settingsTemplate.getPlatformSettings(_settings, _platform, _dub.project.rootPackage.path); 471 return _compiler !is null; 472 } 473 474 /// Returns the project name 475 string name() const @property 476 { 477 return _dub.projectName; 478 } 479 480 /// Returns the project path 481 auto path() const @property 482 { 483 return _dub.projectPath; 484 } 485 486 /// Returns whether there is a target set to build. If this is false then build will throw an exception. 487 bool canBuild() const @property 488 { 489 if (_settings.targetType == TargetType.none || _settings.targetType == TargetType.sourceLibrary 490 || !_dub.project.configurations.canFind(_configuration)) 491 return false; 492 return true; 493 } 494 495 /// Asynchroniously builds the project WITHOUT OUTPUT. This is intended for linting code and showing build errors quickly inside the IDE. 496 Future!(BuildIssue[]) build() 497 { 498 import std.process : thisProcessID; 499 import std.file : tempDir; 500 import std.random : uniform; 501 502 validateBuildConfiguration(); 503 504 // copy to this thread 505 auto compiler = _compiler; 506 auto buildPlatform = _platform; 507 508 GeneratorSettings settings; 509 settings.platform = buildPlatform; 510 settings.config = _configuration; 511 settings.buildType = _buildType; 512 settings.compiler = compiler; 513 settings.buildSettings = _settings; 514 515 auto ret = new typeof(return); 516 new Thread({ 517 try 518 { 519 auto issues = appender!(BuildIssue[]); 520 521 settings.compileCallback = (status, output) { 522 string[] lines = output.splitLines; 523 foreach (line; lines) 524 { 525 auto match = line.matchFirst(errorFormat); 526 if (match) 527 { 528 issues ~= BuildIssue(match[2].to!int, match[3].toOr!int(0), 529 match[1], match[4].to!ErrorType, match[5]); 530 } 531 else 532 { 533 auto contMatch = line.matchFirst(errorFormatCont); 534 if (issues.data.length && contMatch) 535 { 536 issues ~= BuildIssue(contMatch[2].to!int, 537 contMatch[3].toOr!int(1), contMatch[1], 538 issues.data[$ - 1].type, contMatch[4], true); 539 } 540 else if (line.canFind("is deprecated")) 541 { 542 auto deprMatch = line.matchFirst(deprecationFormat); 543 if (deprMatch) 544 { 545 issues ~= BuildIssue(deprMatch[2].to!int, deprMatch[3].toOr!int(1), 546 deprMatch[1], ErrorType.Deprecation, 547 deprMatch[4] ~ " is deprecated, use " ~ deprMatch[5] ~ " instead."); 548 } 549 } 550 } 551 } 552 }; 553 try 554 { 555 import workspaced.dub.lintgenerator : DubLintGenerator; 556 557 new DubLintGenerator(_dub.project).generate(settings); 558 } 559 catch (Exception e) 560 { 561 if (!e.msg.matchFirst(harmlessExceptionFormat)) 562 throw e; 563 } 564 ret.finish(issues.data); 565 } 566 catch (Throwable t) 567 { 568 ret.error(t); 569 } 570 }).start(); 571 return ret; 572 } 573 574 /// Converts the root package recipe to another format. 575 /// Params: 576 /// format = either "json" or "sdl". 577 string convertRecipe(string format) 578 { 579 import dub.recipe.io : serializePackageRecipe; 580 import std.array : appender; 581 582 auto dst = appender!string; 583 serializePackageRecipe(dst, _dub.project.rootPackage.rawRecipe, "dub." ~ format); 584 return dst.data; 585 } 586 587 /// Tries to find a suitable code byte range where a given dub build issue 588 /// applies to. 589 /// Returns: `[pos, pos]` if not found, otherwise range in bytes which might 590 /// not contain the position at all. 591 int[2] resolveDiagnosticRange(scope const(char)[] code, int position, 592 scope const(char)[] diagnostic) 593 { 594 import dparse.lexer : getTokensForParser, LexerConfig; 595 import dparse.parser : parseModule; 596 import dparse.rollback_allocator : RollbackAllocator; 597 import workspaced.dub.diagnostics : resolveDubDiagnosticRange; 598 599 LexerConfig config; 600 RollbackAllocator rba; 601 auto tokens = getTokensForParser(cast(ubyte[]) code, config, &workspaced.stringCache); 602 auto parsed = parseModule(tokens, "equal_finder.d", &rba); 603 604 return resolveDubDiagnosticRange(code, tokens, parsed, position, diagnostic); 605 } 606 607 private: 608 Dub _dub; 609 bool _dubRunning = false; 610 string _configuration; 611 string _archType = "x86_64"; 612 string _buildType = "debug"; 613 string _compilerBinaryName; 614 Compiler _compiler; 615 BuildSettingsTemplate _settingsTemplate; 616 BuildSettings _settings; 617 BuildPlatform _platform; 618 string[] _importPaths, _stringImportPaths, _importFiles, _versions, _debugVersions; 619 } 620 621 /// 622 enum ErrorType : ubyte 623 { 624 /// 625 Error = 0, 626 /// 627 Warning = 1, 628 /// 629 Deprecation = 2 630 } 631 632 /// Returned by build 633 struct BuildIssue 634 { 635 /// 636 int line, column; 637 /// 638 string file; 639 /// 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) 640 ErrorType type; 641 /// 642 string text; 643 /// true if this is additional error information for the last error. 644 bool cont; 645 } 646 647 /// returned by rootPackageBuildSettings 648 struct PackageBuildSettings 649 { 650 /// construct from dub build settings 651 this(BuildSettings dubBuildSettings, string packagePath, string packageName, string recipePath) 652 { 653 foreach (i, ref val; this.tupleof[0 .. __IGNORE_TRAIL]) 654 { 655 enum name = __traits(identifier, this.tupleof[i]); 656 static if (__traits(hasMember, dubBuildSettings, name)) 657 val = __traits(getMember, dubBuildSettings, name); 658 } 659 this.packagePath = packagePath; 660 this.packageName = packageName; 661 this.recipePath = recipePath; 662 663 if (!targetName.length) 664 targetName = packageName; 665 666 version (Windows) 667 targetName ~= ".exe"; 668 669 this.targetType = dubBuildSettings.targetType.to!string; 670 foreach (enumMember; __traits(allMembers, BuildOption)) 671 { 672 enum value = __traits(getMember, BuildOption, enumMember); 673 if (value != 0 && dubBuildSettings.options.opDispatch!enumMember) 674 this.buildOptions ~= enumMember; 675 } 676 foreach (enumMember; __traits(allMembers, BuildRequirement)) 677 { 678 enum value = __traits(getMember, BuildRequirement, enumMember); 679 if (value != 0 && dubBuildSettings.requirements.opDispatch!enumMember) 680 this.buildRequirements ~= enumMember; 681 } 682 } 683 684 string packagePath; 685 string packageName; 686 string recipePath; 687 688 string targetPath; /// same as dub BuildSettings 689 string targetName; /// same as dub BuildSettings 690 string workingDirectory; /// same as dub BuildSettings 691 string mainSourceFile; /// same as dub BuildSettings 692 string[] dflags; /// same as dub BuildSettings 693 string[] lflags; /// same as dub BuildSettings 694 string[] libs; /// same as dub BuildSettings 695 string[] linkerFiles; /// same as dub BuildSettings 696 string[] sourceFiles; /// same as dub BuildSettings 697 string[] copyFiles; /// same as dub BuildSettings 698 string[] extraDependencyFiles; /// same as dub BuildSettings 699 string[] versions; /// same as dub BuildSettings 700 string[] debugVersions; /// same as dub BuildSettings 701 string[] versionFilters; /// same as dub BuildSettings 702 string[] debugVersionFilters; /// same as dub BuildSettings 703 string[] importPaths; /// same as dub BuildSettings 704 string[] stringImportPaths; /// same as dub BuildSettings 705 string[] importFiles; /// same as dub BuildSettings 706 string[] stringImportFiles; /// same as dub BuildSettings 707 string[] preGenerateCommands; /// same as dub BuildSettings 708 string[] postGenerateCommands; /// same as dub BuildSettings 709 string[] preBuildCommands; /// same as dub BuildSettings 710 string[] postBuildCommands; /// same as dub BuildSettings 711 string[] preRunCommands; /// same as dub BuildSettings 712 string[] postRunCommands; /// same as dub BuildSettings 713 714 private enum __IGNORE_TRAIL = 2; // number of ignored settings below this line 715 716 string targetType; /// same as dub BuildSettings 717 string[] buildOptions; /// same as dub BuildSettings 718 string[] buildRequirements; /// same as dub BuildSettings 719 } 720 721 private: 722 723 T toOr(T)(string s, T defaultValue) 724 { 725 if (!s || !s.length) 726 return defaultValue; 727 return s.to!T; 728 } 729 730 enum harmlessExceptionFormat = ctRegex!(`failed with exit code`, "g"); 731 enum errorFormat = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\): (Deprecation|Warning|Error): (.*)`, "gi"); 732 enum errorFormatCont = ctRegex!(`(.*?)\((\d+)(?:,(\d+))?\):[ ]{6,}(.*)`, "g"); 733 enum deprecationFormat = ctRegex!( 734 `(.*?)\((\d+)(?:,(\d+))?\): (.*?) is deprecated, use (.*?) instead.$`, "g"); 735 736 struct DubPackageInfo 737 { 738 string[string] dependencies; 739 string ver; 740 string name; 741 string path; 742 string description; 743 string homepage; 744 const(string)[] authors; 745 string copyright; 746 string license; 747 DubPackageInfo[] subPackages; 748 749 void fill(in PackageRecipe recipe) 750 { 751 description = recipe.description; 752 homepage = recipe.homepage; 753 authors = recipe.authors; 754 copyright = recipe.copyright; 755 license = recipe.license; 756 757 foreach (subpackage; recipe.subPackages) 758 { 759 DubPackageInfo info; 760 info.ver = subpackage.recipe.version_; 761 info.name = subpackage.recipe.name; 762 info.path = subpackage.path; 763 info.fill(subpackage.recipe); 764 } 765 } 766 } 767 768 DubPackageInfo getInfo(in Package dep) 769 { 770 DubPackageInfo info; 771 info.name = dep.name; 772 info.ver = dep.version_.toString; 773 info.path = dep.path.toString; 774 info.fill(dep.recipe); 775 foreach (subDep; dep.getAllDependencies()) 776 { 777 info.dependencies[subDep.name] = subDep.spec.toString; 778 } 779 return info; 780 } 781 782 auto listDependencies(scope const Project project) 783 { 784 auto deps = project.dependencies; 785 DubPackageInfo[] dependencies; 786 if (deps is null) 787 return dependencies; 788 foreach (dep; deps) 789 { 790 dependencies ~= getInfo(dep); 791 } 792 return dependencies; 793 } 794 795 string[] listDependencies(scope const Package pkg) 796 { 797 auto deps = pkg.getAllDependencies(); 798 string[] dependencies; 799 if (deps is null) 800 return dependencies; 801 foreach (dep; deps) 802 dependencies ~= dep.name; 803 return dependencies; 804 }