1 module dlangide.workspace.project; 2 3 import dlangide.workspace.workspace; 4 import dlangide.workspace.projectsettings; 5 import dlangui.core.logger; 6 import dlangui.core.collections; 7 import dlangui.core.settings; 8 import std.algorithm; 9 import std.array : empty; 10 import std.file; 11 import std.path; 12 import std.process; 13 import std.utf; 14 import std.ascii : isAlphaNum; 15 16 string[] includePath; 17 18 /// return true if filename matches rules for workspace file names 19 bool isProjectFile(in string filename) pure nothrow { 20 return filename.baseName.equal("dub.json") || filename.baseName.equal("DUB.JSON") || filename.baseName.equal("package.json") || 21 filename.baseName.equal("dub.sdl") || filename.baseName.equal("DUB.SDL"); 22 } 23 24 string toForwardSlashSeparator(in string filename) pure nothrow { 25 char[] res; 26 foreach(ch; filename) { 27 if (ch == '\\') 28 res ~= '/'; 29 else 30 res ~= ch; 31 } 32 return cast(string)res; 33 } 34 35 /// project item 36 class ProjectItem { 37 protected Project _project; 38 protected ProjectItem _parent; 39 protected string _filename; 40 protected dstring _name; 41 42 this(string filename) { 43 _filename = buildNormalizedPath(filename); 44 _name = toUTF32(baseName(_filename)); 45 } 46 47 this() { 48 } 49 50 @property ProjectItem parent() { return _parent; } 51 52 @property Project project() { return _project; } 53 54 @property void project(Project p) { _project = p; } 55 56 @property string filename() { return _filename; } 57 58 @property string directory() { 59 import std.path : dirName; 60 return _filename.dirName; 61 } 62 63 @property dstring name() { return _name; } 64 65 @property string name8() { 66 return _name.toUTF8; 67 } 68 69 /// returns true if item is folder 70 @property const bool isFolder() { return false; } 71 /// returns child object count 72 @property int childCount() { return 0; } 73 /// returns child item by index 74 ProjectItem child(int index) { return null; } 75 76 void refresh() { 77 } 78 79 ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) { 80 if (fullFileName.equal(_filename)) 81 return cast(ProjectSourceFile)this; 82 if (project && projectFileName.equal(project.absoluteToRelativePath(_filename))) 83 return cast(ProjectSourceFile)this; 84 return null; 85 } 86 87 @property bool isDSourceFile() { 88 if (isFolder) 89 return false; 90 return filename.endsWith(".d") || filename.endsWith(".dd") || filename.endsWith(".dd") || filename.endsWith(".di") || filename.endsWith(".dh") || filename.endsWith(".ddoc"); 91 } 92 93 @property bool isJsonFile() { 94 if (isFolder) 95 return false; 96 return filename.endsWith(".json") || filename.endsWith(".JSON"); 97 } 98 99 @property bool isDMLFile() { 100 if (isFolder) 101 return false; 102 return filename.endsWith(".dml") || filename.endsWith(".DML"); 103 } 104 105 @property bool isXMLFile() { 106 if (isFolder) 107 return false; 108 return filename.endsWith(".xml") || filename.endsWith(".XML"); 109 } 110 } 111 112 /// Project folder 113 class ProjectFolder : ProjectItem { 114 protected ObjectList!ProjectItem _children; 115 116 this(string filename) { 117 super(filename); 118 } 119 120 @property override const bool isFolder() { 121 return true; 122 } 123 @property override int childCount() { 124 return _children.count; 125 } 126 /// returns child item by index 127 override ProjectItem child(int index) { 128 return _children[index]; 129 } 130 void addChild(ProjectItem item) { 131 _children.add(item); 132 item._parent = this; 133 item._project = _project; 134 } 135 ProjectItem childByPathName(string path) { 136 for (int i = 0; i < _children.count; i++) { 137 if (_children[i].filename.equal(path)) 138 return _children[i]; 139 } 140 return null; 141 } 142 ProjectItem childByName(dstring s) { 143 for (int i = 0; i < _children.count; i++) { 144 if (_children[i].name.equal(s)) 145 return _children[i]; 146 } 147 return null; 148 } 149 150 override ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) { 151 for (int i = 0; i < _children.count; i++) { 152 if (ProjectSourceFile res = _children[i].findSourceFile(projectFileName, fullFileName)) 153 return res; 154 } 155 return null; 156 } 157 158 bool loadDir(string path) { 159 string src = relativeToAbsolutePath(path); 160 if (exists(src) && isDir(src)) { 161 ProjectFolder existing = cast(ProjectFolder)childByPathName(src); 162 if (existing) { 163 if (existing.isFolder) 164 existing.loadItems(); 165 return true; 166 } 167 auto dir = new ProjectFolder(src); 168 addChild(dir); 169 Log.d(" added project folder ", src); 170 dir.loadItems(); 171 return true; 172 } 173 return false; 174 } 175 176 bool loadFile(string path) { 177 string src = relativeToAbsolutePath(path); 178 if (exists(src) && isFile(src)) { 179 ProjectItem existing = childByPathName(src); 180 if (existing) 181 return true; 182 auto f = new ProjectSourceFile(src); 183 addChild(f); 184 Log.d(" added project file ", src); 185 return true; 186 } 187 return false; 188 } 189 190 void loadItems() { 191 bool[string] loaded; 192 string path = _filename; 193 if (exists(path) && isFile(path)) 194 path = dirName(path); 195 foreach(e; dirEntries(path, SpanMode.shallow)) { 196 string fn = baseName(e.name); 197 if (e.isDir) { 198 loadDir(fn); 199 loaded[fn] = true; 200 } else if (e.isFile) { 201 loadFile(fn); 202 loaded[fn] = true; 203 } 204 } 205 // removing non-reloaded items 206 for (int i = _children.count - 1; i >= 0; i--) { 207 if (!(toUTF8(_children[i].name) in loaded)) { 208 _children.remove(i); 209 } 210 } 211 sortItems(); 212 } 213 214 /// predicate for sorting project items 215 static bool compareProjectItemsLess(ProjectItem item1, ProjectItem item2) { 216 return ((item1.isFolder && !item2.isFolder) || ((item1.isFolder == item2.isFolder) && (item1.name < item2.name))); 217 } 218 219 void sortItems() { 220 import std.algorithm.sorting : sort; 221 sort!compareProjectItemsLess(_children.asArray); 222 } 223 224 string relativeToAbsolutePath(string path) { 225 if (isAbsolute(path)) 226 return path; 227 string fn = _filename; 228 if (exists(fn) && isFile(fn)) 229 fn = dirName(fn); 230 return buildNormalizedPath(fn, path); 231 } 232 233 override void refresh() { 234 loadItems(); 235 } 236 } 237 238 239 /// Project source file 240 class ProjectSourceFile : ProjectItem { 241 this(string filename) { 242 super(filename); 243 } 244 /// file path relative to project directory 245 @property string projectFilePath() { 246 return project.absoluteToRelativePath(filename); 247 } 248 249 void setFilename(string filename) { 250 _filename = buildNormalizedPath(filename); 251 _name = toUTF32(baseName(_filename)); 252 } 253 } 254 255 class WorkspaceItem { 256 protected string _filename; 257 protected string _dir; 258 protected dstring _name; 259 protected dstring _originalName; 260 protected dstring _description; 261 262 this(string fname = null) { 263 filename = fname; 264 } 265 266 /// file name of workspace item 267 @property string filename() { return _filename; } 268 269 /// workspace item directory 270 @property string dir() { return _dir; } 271 272 /// file name of workspace item 273 @property void filename(string fname) { 274 if (fname.length > 0) { 275 _filename = buildNormalizedPath(fname); 276 _dir = dirName(filename); 277 } else { 278 _filename = null; 279 _dir = null; 280 } 281 } 282 283 /// name 284 @property dstring name() { return _name; } 285 286 @property string name8() { 287 return _name.toUTF8; 288 } 289 290 /// name 291 @property void name(dstring s) { _name = s; } 292 293 /// description 294 @property dstring description() { return _description; } 295 /// description 296 @property void description(dstring s) { _description = s; } 297 298 /// load 299 bool load(string fname) { 300 // override it 301 return false; 302 } 303 304 bool save(string fname = null) { 305 return false; 306 } 307 } 308 309 /// Stores info about project configuration 310 struct ProjectConfiguration { 311 /// name used to build the project 312 string name; 313 /// type, for libraries one can run tests, for apps - execute them 314 Type type; 315 316 /// How to display default configuration in ui 317 immutable static string DEFAULT_NAME = "default"; 318 /// Default project configuration 319 immutable static ProjectConfiguration DEFAULT = ProjectConfiguration(DEFAULT_NAME, Type.Default); 320 321 /// Type of configuration 322 enum Type { 323 Default, 324 Executable, 325 Library 326 } 327 328 private static Type parseType(string s) 329 { 330 switch(s) 331 { 332 case "executable": return Type.Executable; 333 case "library": return Type.Library; 334 case "dynamicLibrary": return Type.Library; 335 case "staticLibrary": return Type.Library; 336 default: return Type.Default; 337 } 338 } 339 340 /// parsing from setting file 341 static ProjectConfiguration[] load(Setting s) 342 { 343 ProjectConfiguration[] res; 344 Setting configs = s.objectByPath("configurations"); 345 if(configs is null || configs.type != SettingType.ARRAY) { 346 res ~= DEFAULT; 347 return res; 348 } 349 350 foreach(conf; configs) { 351 if(!conf.isObject) continue; 352 Type t = Type.Default; 353 if(auto typeName = conf.getString("targetType")) 354 t = parseType(typeName); 355 if (string confName = conf.getString("name")) { 356 res ~= ProjectConfiguration(confName, t); 357 } 358 } 359 return res; 360 } 361 } 362 363 /// DLANGIDE D project 364 class Project : WorkspaceItem { 365 import dlangide.workspace.idesettings : IDESettings; 366 protected Workspace _workspace; 367 protected bool _opened; 368 protected ProjectFolder _items; 369 protected ProjectSourceFile _mainSourceFile; 370 protected SettingsFile _projectFile; 371 protected ProjectSettings _settingsFile; 372 protected bool _isDependency; 373 protected bool _isSubproject; 374 protected bool _isEmbeddedSubproject; 375 protected dstring _baseProjectName; 376 protected string _dependencyVersion; 377 378 protected string[] _sourcePaths; 379 protected ProjectConfiguration[] _configurations; 380 protected ProjectConfiguration _projectConfiguration = ProjectConfiguration.DEFAULT; 381 382 @property int projectConfigurationIndex() { 383 ProjectConfiguration config = projectConfiguration(); 384 foreach(i, value; _configurations) { 385 if (value.name == config.name) 386 return cast(int)i; 387 } 388 return 0; 389 } 390 391 @property ProjectConfiguration projectConfiguration() { 392 if (_configurations.length == 0) 393 return ProjectConfiguration.DEFAULT; 394 string configName = settings.projectConfiguration; 395 foreach(config; _configurations) { 396 if (configName == config.name) 397 return config; 398 } 399 return _configurations[0]; 400 } 401 402 @property void projectConfiguration(ProjectConfiguration config) { 403 settings.projectConfiguration = config.name; 404 settings.save(); 405 } 406 407 @property void projectConfiguration(string configName) { 408 foreach(name, config; _configurations) { 409 if (configName == config.name) { 410 settings.projectConfiguration = config.name; 411 settings.save(); 412 return; 413 } 414 } 415 } 416 417 this(Workspace ws, string fname = null, string dependencyVersion = null) { 418 super(fname); 419 _workspace = ws; 420 421 if (_workspace) { 422 foreach(obj; _workspace.includePath.array) 423 includePath ~= obj.str; 424 } 425 426 _items = new ProjectFolder(fname.dirName); 427 _dependencyVersion = dependencyVersion; 428 _isDependency = _dependencyVersion.length > 0; 429 _projectFile = new SettingsFile(fname); 430 } 431 432 void setBaseProject(Project p) { 433 if (p) { 434 _isSubproject = true; 435 _isDependency = p._isDependency; 436 _baseProjectName = p._originalName; 437 _dependencyVersion = p._dependencyVersion; 438 } else { 439 _isSubproject = false; 440 } 441 } 442 443 void setSubprojectJson(Setting s) { 444 if (!_projectFile) 445 _projectFile = new SettingsFile(); 446 _isEmbeddedSubproject = true; 447 _projectFile.replaceSetting(s.clone); 448 } 449 450 @property ProjectSettings settings() { 451 if (!_settingsFile) { 452 _settingsFile = new ProjectSettings(settingsFileName); 453 _settingsFile.updateDefaults(); 454 _settingsFile.load(); 455 _settingsFile.save(); 456 } 457 return _settingsFile; 458 } 459 460 @property string settingsFileName() { 461 return buildNormalizedPath(dir, toUTF8(name) ~ ".settings"); 462 } 463 464 @property bool isDependency() { return _isDependency; } 465 @property string dependencyVersion() { return _dependencyVersion; } 466 467 /// returns project configurations 468 @property const(ProjectConfiguration[]) configurations() const 469 { 470 return _configurations; 471 } 472 473 /// returns project configurations 474 @property dstring[] configurationNames() const 475 { 476 dstring[] res; 477 res.assumeSafeAppend; 478 foreach(conf; _configurations) 479 res ~= conf.name.toUTF32; 480 return res; 481 } 482 483 /// direct access to project file (json) 484 @property SettingsFile content() { return _projectFile; } 485 486 /// name 487 override @property dstring name() { 488 return super.name(); 489 } 490 491 /// name 492 override @property void name(dstring s) { 493 super.name(s); 494 _projectFile.setString("name", toUTF8(s)); 495 } 496 497 /// name 498 override @property dstring description() { 499 return super.description(); 500 } 501 502 /// name 503 override @property void description(dstring s) { 504 super.description(s); 505 _projectFile.setString("description", toUTF8(s)); 506 } 507 508 /// returns project's own source paths 509 @property string[] sourcePaths() { return _sourcePaths; } 510 /// returns project's current toolchain import paths 511 string[] builderSourcePaths(IDESettings ideSettings) { 512 string compilerName = settings.getToolchain(ideSettings); 513 if (!compilerName) 514 compilerName = "default"; 515 return compilerImportPathsCache.getImportPathsFor(compilerName); 516 } 517 518 /// returns first source folder for project or null if not found 519 ProjectFolder firstSourceFolder() { 520 for(int i = 0; i < _items.childCount; i++) { 521 if (_items.child(i).isFolder) 522 return cast(ProjectFolder)_items.child(i); 523 } 524 return null; 525 } 526 527 ProjectSourceFile findSourceFile(string projectFileName, string fullFileName) { 528 return _items ? _items.findSourceFile(projectFileName, fullFileName) : null; 529 } 530 531 private static void addUnique(ref string[] dst, string[] items) { 532 foreach(item; items) { 533 if (!canFind(dst, item)) 534 dst ~= item; 535 } 536 } 537 @property string[] importPaths(IDESettings ideSettings) { 538 string[] res; 539 addUnique(res, sourcePaths); 540 addUnique(res, builderSourcePaths(ideSettings)); 541 foreach(dep; _dependencies) { 542 addUnique(res, dep.sourcePaths); 543 } 544 return res; 545 } 546 547 string relativeToAbsolutePath(string path) { 548 if (isAbsolute(path)) 549 return path; 550 return buildNormalizedPath(_dir, path); 551 } 552 553 string absoluteToRelativePath(string path) { 554 if (!isAbsolute(path)) 555 return path; 556 return relativePath(path, _dir); 557 } 558 559 @property ProjectSourceFile mainSourceFile() { return _mainSourceFile; } 560 @property ProjectFolder items() { return _items; } 561 562 @property Workspace workspace() { return _workspace; } 563 564 @property void workspace(Workspace p) { _workspace = p; } 565 566 @property string defWorkspaceFile() { 567 return buildNormalizedPath(_filename.dirName, toUTF8(name) ~ WORKSPACE_EXTENSION); 568 } 569 570 @property bool isExecutable() { 571 // TODO: use targetType 572 return true; 573 } 574 575 /// return executable file name, or null if it's library project or executable is not found 576 @property string executableFileName() { 577 if (!isExecutable) 578 return null; 579 string exename = toUTF8(name); 580 exename = _projectFile.getString("targetName", exename); 581 // TODO: use targetName 582 version (Windows) { 583 exename = exename ~ ".exe"; 584 } 585 string targetPath = _projectFile.getString("targetPath", null); 586 string exePath; 587 if (targetPath.length) 588 exePath = buildNormalizedPath(_filename.dirName, targetPath, exename); // int $targetPath directory 589 else 590 exePath = buildNormalizedPath(_filename.dirName, exename); // in project directory 591 return exePath; 592 } 593 594 /// working directory for running and debugging project 595 @property string workingDirectory() { 596 // TODO: get from settings 597 return _filename.dirName; 598 } 599 600 /// commandline parameters for running and debugging project 601 @property string runArgs() { 602 // TODO: get from settings 603 return null; 604 } 605 606 @property bool runInExternalConsole() { 607 // TODO 608 return settings.runInExternalConsole; 609 } 610 611 private ProjectFolder findItems(string[] srcPaths) { 612 auto folder = new ProjectFolder(_filename.dirName); 613 folder.project = this; 614 foreach(customPath; srcPaths) { 615 string path = relativeToAbsolutePath(customPath); 616 if (folder.loadDir(path)) 617 _sourcePaths ~= path; 618 } 619 return folder; 620 } 621 622 void refresh() { 623 for (int i = _items._children.count - 1; i >= 0; i--) { 624 if (_items._children[i].isFolder) 625 _items._children[i].refresh(); 626 } 627 } 628 629 void findMainSourceFile() { 630 string n = toUTF8(name); 631 string[] mainnames = ["app.d", "main.d", n ~ ".d"]; 632 foreach(sname; mainnames) { 633 _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "src", sname)); 634 if (_mainSourceFile) 635 break; 636 _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "source", sname)); 637 if (_mainSourceFile) 638 break; 639 } 640 } 641 642 /// tries to find source file in project, returns found project source file item, or null if not found 643 ProjectSourceFile findSourceFileItem(ProjectItem dir, string filename, bool fullFileName=true) { 644 foreach(i; 0 .. dir.childCount) { 645 ProjectItem item = dir.child(i); 646 if (item.isFolder) { 647 ProjectSourceFile res = findSourceFileItem(item, filename, fullFileName); 648 if (res) 649 return res; 650 } else { 651 auto res = cast(ProjectSourceFile)item; 652 if(res) 653 { 654 if(fullFileName && res.filename.equal(filename)) 655 return res; 656 else if (!fullFileName && res.filename.endsWith(filename)) 657 return res; 658 } 659 } 660 } 661 return null; 662 } 663 664 ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true) { 665 return findSourceFileItem(_items, filename, fullFileName); 666 } 667 668 protected Project[] _subPackages; 669 670 /// add item to string array ignoring duplicates 671 protected static void addUnique(ref string[] list, string item) { 672 foreach(s; list) 673 if (s == item) 674 return; 675 list ~= item; 676 } 677 678 /// add item to string array ignoring duplicates 679 protected void addRelativePathIfExists(ref string[] list, string item) { 680 item = relativeToAbsolutePath(item); 681 if (item.exists && item.isDir) 682 addUnique(list, item); 683 } 684 685 /// find source paths for project 686 protected string[] findSourcePaths() { 687 string[] res; 688 res.assumeSafeAppend; 689 string[] srcPaths = _projectFile.getStringArray("sourcePaths"); 690 foreach(s; srcPaths) 691 addRelativePathIfExists(res, s); 692 Setting configs = _projectFile.objectByPath("configurations"); 693 if (configs) { 694 for (int i = 0; i < configs.length; i++) { 695 Setting s = configs[i]; 696 if (s) { 697 string[] paths = s.getStringArray("sourcePaths"); 698 foreach(path; paths) 699 addRelativePathIfExists(res, path); 700 } 701 } 702 } 703 if (!res.length) { 704 addRelativePathIfExists(res, "src"); 705 addRelativePathIfExists(res, "source"); 706 } 707 708 return res; 709 } 710 711 void processSubpackages() { 712 import dlangui.core.files; 713 _subPackages.length = 0; 714 Setting subPackages = _projectFile.settingByPath("subPackages", SettingType.ARRAY, false); 715 if (subPackages) { 716 string p = _projectFile.filename.dirName; 717 for(int i = 0; i < subPackages.length; i++) { 718 Setting sp = subPackages[i]; 719 if (sp.isString) { 720 // string 721 string path = convertPathDelimiters(sp.str); 722 string relative = relativePath(path, p); 723 path = buildNormalizedPath(absolutePath(relative, p)); 724 //Log.d("Subproject path: ", path); 725 string fn = DubPackageFinder.findPackageFile(path); 726 //Log.d("Subproject file: ", fn); 727 Project prj = new Project(_workspace, fn); 728 prj.setBaseProject(this); 729 if (prj.load()) { 730 Log.d("Loaded subpackage from file: ", fn); 731 _subPackages ~= prj; 732 if (_workspace) 733 _workspace.addDependencyProject(prj); 734 } else { 735 Log.w("Failed to load subpackage from file: ", fn); 736 } 737 } else if (sp.isObject) { 738 // object - file inside base project dub.json 739 Log.d("Subpackage is JSON object"); 740 string subname = sp.getString("name"); 741 if (subname) { 742 Project prj = new Project(_workspace, _filename ~ "@" ~ subname); 743 prj.setBaseProject(this); 744 prj.setSubprojectJson(sp); 745 bool res = prj.processLoadedProject(); 746 if (res) { 747 Log.d("Added embedded subpackage ", subname); 748 _subPackages ~= prj; 749 if (_workspace) 750 _workspace.addDependencyProject(prj); 751 } else { 752 Log.w("Error while processing embedded subpackage"); 753 } 754 } 755 } 756 } 757 } 758 } 759 760 /// parse data from _projectFile after loading 761 bool processLoadedProject() { 762 // 763 _mainSourceFile = null; 764 try { 765 _name = toUTF32(_projectFile.getString("name")); 766 _originalName = _name; 767 if (_baseProjectName) { 768 _name = _baseProjectName ~ ":" ~ _name; 769 } 770 if (_isDependency) { 771 _name ~= "-"d; 772 _name ~= toUTF32(_dependencyVersion.startsWith("~") ? _dependencyVersion[1..$] : _dependencyVersion); 773 } 774 _description = toUTF32(_projectFile.getString("description")); 775 Log.d(" project name: ", _name); 776 Log.d(" project description: ", _description); 777 778 processSubpackages(); 779 780 string[] srcPaths = findSourcePaths(); 781 _sourcePaths = null; 782 _items = findItems(srcPaths); 783 findMainSourceFile(); 784 785 Log.i("Project source paths: ", sourcePaths); 786 //Log.i("Builder source paths: ", builderSourcePaths(_settings)); 787 if (!_isDependency) 788 loadSelections(); 789 790 _configurations = ProjectConfiguration.load(_projectFile); 791 Log.i("Project configurations: ", _configurations); 792 793 794 } catch (Exception e) { 795 Log.e("Cannot read project file", e); 796 return false; 797 } 798 _items.loadFile(filename); 799 return true; 800 } 801 802 override bool load(string fname = null) { 803 if (!_projectFile) 804 _projectFile = new SettingsFile(); 805 if (fname.length > 0) 806 filename = fname; 807 if (!_projectFile.load(_filename)) { 808 Log.e("failed to load project from file ", _filename); 809 return false; 810 } 811 Log.d("Reading project from file ", _filename); 812 return processLoadedProject(); 813 814 } 815 816 override bool save(string fname = null) { 817 if (_isEmbeddedSubproject) 818 return false; 819 if (fname !is null) 820 filename = fname; 821 assert(filename !is null); 822 return _projectFile.save(filename, true); 823 } 824 825 protected Project[] _dependencies; 826 @property Project[] dependencies() { return _dependencies; } 827 828 Project findDependencyProject(string filename) { 829 foreach(dep; _dependencies) { 830 if (dep.filename.equal(filename)) 831 return dep; 832 } 833 return null; 834 } 835 836 bool loadSelections() { 837 Project[] newdeps; 838 _dependencies.length = 0; 839 auto finder = new DubPackageFinder; 840 scope(exit) destroy(finder); 841 SettingsFile selectionsFile = new SettingsFile(buildNormalizedPath(_dir, "dub.selections.json")); 842 if (!selectionsFile.load()) { 843 _dependencies = newdeps; 844 return false; 845 } 846 Setting versions = selectionsFile.objectByPath("versions"); 847 if (!versions.isObject) { 848 _dependencies = newdeps; 849 return false; 850 } 851 string[string] versionMap = versions.strMap; 852 foreach(packageName, packageVersion; versionMap) { 853 string fn = finder.findPackage(packageName, packageVersion); 854 Log.d("dependency ", packageName, " ", packageVersion, " : ", fn ? fn : "NOT FOUND"); 855 if (fn) { 856 Project p = findDependencyProject(fn); 857 if (p) { 858 Log.d("Found existing dependency project ", fn); 859 newdeps ~= p; 860 continue; 861 } 862 p = new Project(_workspace, fn, packageVersion); 863 if (p.load()) { 864 newdeps ~= p; 865 if (_workspace) 866 _workspace.addDependencyProject(p); 867 } else { 868 Log.e("cannot load dependency package ", packageName, " ", packageVersion, " from file ", fn); 869 destroy(p); 870 } 871 } 872 } 873 _dependencies = newdeps; 874 return true; 875 } 876 } 877 878 class DubPackageFinder { 879 string systemDubPath; 880 string userDubPath; 881 string tempPath; 882 this() { 883 version(Windows){ 884 systemDubPath = buildNormalizedPath(environment.get("ProgramData"), "dub", "packages"); 885 userDubPath = buildNormalizedPath(environment.get("APPDATA"), "dub", "packages"); 886 tempPath = buildNormalizedPath(environment.get("TEMP"), "dub", "packages"); 887 } else version(Posix){ 888 systemDubPath = "/var/lib/dub/packages"; 889 userDubPath = buildNormalizedPath(environment.get("HOME"), ".dub", "packages"); 890 if(!userDubPath.isAbsolute) 891 userDubPath = buildNormalizedPath(getcwd(), userDubPath); 892 tempPath = "/tmp/packages"; 893 } 894 } 895 896 /// find package file (dub.json, package.json) in specified dir; returns absoulute path to found file or null if not found 897 static string findPackageFile(string pathName) { 898 string fn = buildNormalizedPath(pathName, "dub.json"); 899 if (fn.exists && fn.isFile) 900 return fn; 901 fn = buildNormalizedPath(pathName, "dub.sdl"); 902 if (fn.exists && fn.isFile) 903 return fn; 904 fn = buildNormalizedPath(pathName, "package.json"); 905 if (fn.exists && fn.isFile) 906 return fn; 907 return null; 908 } 909 910 protected string findPackage(string packageDir, string packageName, string packageVersion) { 911 string fullName = packageVersion.startsWith("~") ? packageName ~ "-" ~ packageVersion[1..$] : packageName ~ "-" ~ packageVersion; 912 string pathName = absolutePath(buildNormalizedPath(packageDir, fullName)); 913 if (pathName.exists && pathName.isDir) { 914 string fn = findPackageFile(pathName); 915 if (fn) 916 return fn; 917 // new DUB support - with package subdirectory 918 fn = findPackageFile(buildNormalizedPath(pathName, packageName)); 919 if (fn) 920 return fn; 921 } 922 return null; 923 } 924 925 string findPackage(string packageName, string packageVersion) { 926 string res = null; 927 res = findPackage(userDubPath, packageName, packageVersion); 928 if (res) 929 return res; 930 res = findPackage(systemDubPath, packageName, packageVersion); 931 return res; 932 } 933 } 934 935 bool isValidProjectName(in string s) pure { 936 if (s.empty) 937 return false; 938 return reduce!((a, b) => a && (b == '_' || b == '-' || isAlphaNum(b)))(true, s); 939 } 940 941 bool isValidModuleName(in string s) pure { 942 if (s.empty) 943 return false; 944 return reduce!((a, b) => a && (b == '_' || isAlphaNum(b)))(true, s); 945 } 946 947 bool isValidFileName(in string s) pure { 948 if (s.empty) 949 return false; 950 return reduce!((a, b) => a && (b == '_' || b == '.' || b == '-' || isAlphaNum(b)))(true, s); 951 } 952 953 unittest { 954 assert(!isValidProjectName("")); 955 assert(isValidProjectName("project")); 956 assert(isValidProjectName("cool_project")); 957 assert(isValidProjectName("project-2")); 958 assert(!isValidProjectName("project.png")); 959 assert(!isValidProjectName("[project]")); 960 assert(!isValidProjectName("<project/>")); 961 assert(!isValidModuleName("")); 962 assert(isValidModuleName("module")); 963 assert(isValidModuleName("awesome_module2")); 964 assert(!isValidModuleName("module-2")); 965 assert(!isValidModuleName("module.png")); 966 assert(!isValidModuleName("[module]")); 967 assert(!isValidModuleName("<module>")); 968 assert(!isValidFileName("")); 969 assert(isValidFileName("file")); 970 assert(isValidFileName("file_2")); 971 assert(isValidFileName("file-2")); 972 assert(isValidFileName("file.txt")); 973 assert(!isValidFileName("[file]")); 974 assert(!isValidFileName("<file>")); 975 } 976 977 class EditorBookmark { 978 string file; 979 string fullFilePath; 980 string projectFilePath; 981 int line; 982 string projectName; 983 } 984 985 986 string[] splitByLines(string s) { 987 string[] res; 988 int start = 0; 989 for(int i = 0; i <= s.length; i++) { 990 if (i == s.length) { 991 if (start < i) 992 res ~= s[start .. i]; 993 break; 994 } 995 if (s[i] == '\r' || s[i] == '\n') { 996 if (start < i) 997 res ~= s[start .. i]; 998 start = i + 1; 999 } 1000 } 1001 return res; 1002 } 1003 1004 struct CompilerImportPathsCache { 1005 1006 private static class Entry { 1007 string[] list; 1008 } 1009 private Entry[string] _cache; 1010 1011 string[] getImportPathsFor(string compiler) { 1012 import dlangui.core.files : findExecutablePath; 1013 if (!compiler.length) 1014 return []; 1015 if (auto p = compiler in _cache) { 1016 // found in cache 1017 return p.list; 1018 } 1019 Log.d("Searching for compiler path: ", compiler); 1020 import std.path : isAbsolute; 1021 string compilerPath = compiler; 1022 if (compiler == "default") { 1023 // try to autodetect default compiler 1024 compilerPath = findExecutablePath("dmd"); 1025 if (!compilerPath) 1026 compilerPath = findExecutablePath("ldc"); 1027 if (!compilerPath) 1028 compilerPath = findExecutablePath("gdc"); 1029 } else if (compilerPath && !compilerPath.isAbsolute) 1030 compilerPath = findExecutablePath(compiler); 1031 string[] res; 1032 if (compilerPath) 1033 res = detectImportPathsForCompiler(compilerPath); 1034 else 1035 Log.w("Compiler executable not found for `", compiler, "`"); 1036 Entry newItem = new Entry(); 1037 newItem.list = res; 1038 _cache[compiler] = newItem; 1039 return res; 1040 } 1041 } 1042 1043 __gshared CompilerImportPathsCache compilerImportPathsCache; 1044 1045 string[] detectImportPathsForCompiler(string compiler) { 1046 string[] res; 1047 import std.process : pipeProcess, Redirect, wait; 1048 import std.string : startsWith, indexOf; 1049 import std.path : buildNormalizedPath; 1050 import std.file : write, remove; 1051 import dlangui.core.files; 1052 try { 1053 string sourcefilename = appDataPath(".dlangide") ~ PATH_DELIMITER ~ "tmp_dummy_file_to_get_import_paths.d"; 1054 write(sourcefilename, "import module_that_does_not_exist;\n"); 1055 auto pipes = pipeProcess([compiler, sourcefilename], Redirect.stdin | Redirect.stdout | Redirect.stderrToStdout, null, Config.suppressConsole); 1056 char[4096] buffer; 1057 char[] s = pipes.stdout.rawRead(buffer); 1058 wait(pipes.pid); 1059 //auto ls = execute([compiler, sourcefilename]); 1060 remove(sourcefilename); 1061 //string s = ls.output; 1062 string[] lines = splitByLines(cast(string)s); 1063 debug Log.d("compiler output:\n", s); 1064 foreach(line; lines) { 1065 if (line.startsWith("import path[")) { 1066 auto p = line.indexOf("] = "); 1067 if (p > 0) { 1068 line = line[p + 4 .. $]; 1069 string path = line.buildNormalizedPath; 1070 debug Log.d("import path found: `", path, "`"); 1071 res ~= path; 1072 } 1073 } 1074 } 1075 return res; 1076 } catch (Exception e) { 1077 return null; 1078 } 1079 }