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.path; 9 import std.file; 10 import std.json; 11 import std.utf; 12 import std.algorithm; 13 import std.process; 14 import std.array; 15 16 /// return true if filename matches rules for workspace file names 17 bool isProjectFile(string filename) { 18 return filename.baseName.equal("dub.json") || filename.baseName.equal("package.json"); 19 } 20 21 string toForwardSlashSeparator(string filename) { 22 char[] res; 23 foreach(ch; filename) { 24 if (ch == '\\') 25 res ~= '/'; 26 else 27 res ~= ch; 28 } 29 return cast(string)res; 30 } 31 32 /// project item 33 class ProjectItem { 34 protected Project _project; 35 protected ProjectItem _parent; 36 protected string _filename; 37 protected dstring _name; 38 39 this(string filename) { 40 _filename = buildNormalizedPath(filename); 41 _name = toUTF32(baseName(_filename)); 42 } 43 44 this() { 45 } 46 47 @property ProjectItem parent() { 48 return _parent; 49 } 50 51 @property Project project() { 52 return _project; 53 } 54 55 @property void project(Project p) { 56 _project = p; 57 } 58 59 @property string filename() { 60 return _filename; 61 } 62 63 @property dstring name() { 64 return _name; 65 } 66 67 /// returns true if item is folder 68 @property const bool isFolder() { 69 return false; 70 } 71 /// returns child object count 72 @property int childCount() { 73 return 0; 74 } 75 /// returns child item by index 76 ProjectItem child(int index) { 77 return null; 78 } 79 80 void refresh() { 81 } 82 } 83 84 /// Project folder 85 class ProjectFolder : ProjectItem { 86 protected ObjectList!ProjectItem _children; 87 88 this(string filename) { 89 super(filename); 90 } 91 92 @property override const bool isFolder() { 93 return true; 94 } 95 @property override int childCount() { 96 return _children.count; 97 } 98 /// returns child item by index 99 override ProjectItem child(int index) { 100 return _children[index]; 101 } 102 void addChild(ProjectItem item) { 103 _children.add(item); 104 item._parent = this; 105 item._project = _project; 106 } 107 ProjectItem childByPathName(string path) { 108 for (int i = 0; i < _children.count; i++) { 109 if (_children[i].filename.equal(path)) 110 return _children[i]; 111 } 112 return null; 113 } 114 ProjectItem childByName(dstring s) { 115 for (int i = 0; i < _children.count; i++) { 116 if (_children[i].name.equal(s)) 117 return _children[i]; 118 } 119 return null; 120 } 121 122 bool loadDir(string path) { 123 string src = relativeToAbsolutePath(path); 124 if (exists(src) && isDir(src)) { 125 ProjectFolder existing = cast(ProjectFolder)childByPathName(src); 126 if (existing) { 127 if (existing.isFolder) 128 existing.loadItems(); 129 return true; 130 } 131 ProjectFolder dir = new ProjectFolder(src); 132 addChild(dir); 133 Log.d(" added project folder ", src); 134 dir.loadItems(); 135 return true; 136 } 137 return false; 138 } 139 140 bool loadFile(string path) { 141 string src = relativeToAbsolutePath(path); 142 if (exists(src) && isFile(src)) { 143 ProjectItem existing = childByPathName(src); 144 if (existing) 145 return true; 146 ProjectSourceFile f = new ProjectSourceFile(src); 147 addChild(f); 148 Log.d(" added project file ", src); 149 return true; 150 } 151 return false; 152 } 153 154 void loadItems() { 155 bool[string] loaded; 156 string path = _filename; 157 if (exists(path) && isFile(path)) 158 path = dirName(path); 159 foreach(e; dirEntries(path, SpanMode.shallow)) { 160 string fn = baseName(e.name); 161 if (e.isDir) { 162 loadDir(fn); 163 loaded[fn] = true; 164 } else if (e.isFile) { 165 loadFile(fn); 166 loaded[fn] = true; 167 } 168 } 169 // removing non-reloaded items 170 for (int i = _children.count - 1; i >= 0; i--) { 171 if (!(toUTF8(_children[i].name) in loaded)) { 172 _children.remove(i); 173 } 174 } 175 } 176 177 string relativeToAbsolutePath(string path) { 178 if (isAbsolute(path)) 179 return path; 180 string fn = _filename; 181 if (exists(fn) && isFile(fn)) 182 fn = dirName(fn); 183 return buildNormalizedPath(fn, path); 184 } 185 186 override void refresh() { 187 loadItems(); 188 } 189 } 190 191 /// Project source file 192 class ProjectSourceFile : ProjectItem { 193 this(string filename) { 194 super(filename); 195 } 196 } 197 198 class WorkspaceItem { 199 protected string _filename; 200 protected string _dir; 201 protected dstring _name; 202 protected dstring _description; 203 204 this(string fname = null) { 205 filename = fname; 206 } 207 208 /// file name of workspace item 209 @property string filename() { 210 return _filename; 211 } 212 213 /// workspace item directory 214 @property string dir() { 215 return _dir; 216 } 217 218 /// file name of workspace item 219 @property void filename(string fname) { 220 if (fname.length > 0) { 221 _filename = buildNormalizedPath(fname); 222 _dir = dirName(filename); 223 } else { 224 _filename = null; 225 _dir = null; 226 } 227 } 228 229 /// name 230 @property dstring name() { 231 return _name; 232 } 233 234 /// name 235 @property void name(dstring s) { 236 _name = s; 237 } 238 239 /// name 240 @property dstring description() { 241 return _description; 242 } 243 244 /// name 245 @property void description(dstring s) { 246 _description = s; 247 } 248 249 /// load 250 bool load(string fname) { 251 // override it 252 return false; 253 } 254 255 bool save(string fname = null) { 256 return false; 257 } 258 } 259 260 /// detect DMD source paths 261 string[] dmdSourcePaths() { 262 string[] res; 263 version(Windows) { 264 import dlangui.core.files; 265 string dmdPath = findExecutablePath("dmd"); 266 if (dmdPath) { 267 string dmdDir = buildNormalizedPath(dirName(dmdPath), "..", "..", "src"); 268 res ~= absolutePath(buildNormalizedPath(dmdDir, "druntime", "import")); 269 res ~= absolutePath(buildNormalizedPath(dmdDir, "phobos")); 270 } 271 } else { 272 res ~= "/usr/include/dmd/druntime/import"; 273 res ~= "/usr/include/dmd/phobos"; 274 } 275 return res; 276 } 277 278 /// Stores info about project configuration 279 struct ProjectConfiguration { 280 /// name used to build the project 281 string name; 282 /// type, for libraries one can run tests, for apps - execute them 283 Type type; 284 285 /// How to display default configuration in ui 286 immutable static string DEFAULT_NAME = "default"; 287 /// Default project configuration 288 immutable static ProjectConfiguration DEFAULT = ProjectConfiguration(DEFAULT_NAME, Type.Default); 289 290 /// Type of configuration 291 enum Type { 292 Default, 293 Executable, 294 Library 295 } 296 297 private static Type parseType(string s) 298 { 299 switch(s) 300 { 301 case "executable": return Type.Executable; 302 case "library": return Type.Library; 303 case "dynamicLibrary": return Type.Library; 304 case "staticLibrary": return Type.Library; 305 default: return Type.Default; 306 } 307 } 308 309 /// parsing from setting file 310 static ProjectConfiguration[string] load(Setting s) 311 { 312 ProjectConfiguration[string] res = [DEFAULT_NAME: DEFAULT]; 313 Setting configs = s.objectByPath("configurations"); 314 if(configs is null || configs.type != SettingType.ARRAY) 315 return res; 316 317 foreach(conf; configs) { 318 if(!conf.isObject) continue; 319 Type t = Type.Default; 320 if(auto typeName = conf.getString("targetType")) 321 t = parseType(typeName); 322 if (string confName = conf.getString("name")) 323 res[confName] = ProjectConfiguration(confName, t); 324 } 325 return res; 326 } 327 } 328 329 /// DLANGIDE D project 330 class Project : WorkspaceItem { 331 protected Workspace _workspace; 332 protected bool _opened; 333 protected ProjectFolder _items; 334 protected ProjectSourceFile _mainSourceFile; 335 protected SettingsFile _projectFile; 336 protected ProjectSettings _settingsFile; 337 protected bool _isDependency; 338 protected string _dependencyVersion; 339 340 protected string[] _sourcePaths; 341 protected string[] _builderSourcePaths; 342 protected ProjectConfiguration[string] _configurations; 343 344 this(Workspace ws, string fname = null, string dependencyVersion = null) { 345 super(fname); 346 _workspace = ws; 347 _items = new ProjectFolder(fname); 348 _dependencyVersion = dependencyVersion; 349 _isDependency = _dependencyVersion.length > 0; 350 _projectFile = new SettingsFile(fname); 351 } 352 353 @property ProjectSettings settings() { 354 if (!_settingsFile) { 355 _settingsFile = new ProjectSettings(settingsFileName); 356 _settingsFile.updateDefaults(); 357 _settingsFile.load(); 358 _settingsFile.save(); 359 } 360 return _settingsFile; 361 } 362 363 @property string settingsFileName() { 364 return buildNormalizedPath(dir, toUTF8(name) ~ ".settings"); 365 } 366 367 @property bool isDependency() { return _isDependency; } 368 @property string dependencyVersion() { return _dependencyVersion; } 369 370 /// returns project configurations 371 @property const(ProjectConfiguration[string]) configurations() const 372 { 373 return _configurations; 374 } 375 376 /// direct access to project file (json) 377 @property SettingsFile content() { return _projectFile; } 378 379 /// name 380 override @property dstring name() { 381 return super.name(); 382 } 383 384 /// name 385 override @property void name(dstring s) { 386 super.name(s); 387 _projectFile.setString("name", toUTF8(s)); 388 } 389 390 /// name 391 override @property dstring description() { 392 return super.description(); 393 } 394 395 /// name 396 override @property void description(dstring s) { 397 super.description(s); 398 _projectFile.setString("description", toUTF8(s)); 399 } 400 401 /// returns project's own source paths 402 @property string[] sourcePaths() { return _sourcePaths; } 403 /// returns project's own source paths 404 @property string[] builderSourcePaths() { 405 if (!_builderSourcePaths) { 406 _builderSourcePaths = dmdSourcePaths(); 407 } 408 return _builderSourcePaths; 409 } 410 411 private static void addUnique(ref string[] dst, string[] items) { 412 foreach(item; items) { 413 bool found = false; 414 foreach(existing; dst) { 415 if (item.equal(existing)) { 416 found = true; 417 break; 418 } 419 } 420 if (!found) 421 dst ~= item; 422 } 423 } 424 @property string[] importPaths() { 425 string[] res; 426 addUnique(res, sourcePaths); 427 addUnique(res, builderSourcePaths); 428 foreach(dep; _dependencies) { 429 addUnique(res, dep.sourcePaths); 430 } 431 return res; 432 } 433 434 string relativeToAbsolutePath(string path) { 435 if (isAbsolute(path)) 436 return path; 437 return buildNormalizedPath(_dir, path); 438 } 439 440 @property ProjectSourceFile mainSourceFile() { return _mainSourceFile; } 441 @property ProjectFolder items() { 442 return _items; 443 } 444 445 @property Workspace workspace() { 446 return _workspace; 447 } 448 449 @property void workspace(Workspace p) { 450 _workspace = p; 451 } 452 453 @property string defWorkspaceFile() { 454 return buildNormalizedPath(_filename.dirName, toUTF8(name) ~ WORKSPACE_EXTENSION); 455 } 456 457 @property bool isExecutable() { 458 // TODO: use targetType 459 return true; 460 } 461 462 /// return executable file name, or null if it's library project or executable is not found 463 @property string executableFileName() { 464 if (!isExecutable) 465 return null; 466 string exename = toUTF8(name); 467 // TODO: use targetName 468 version (Windows) { 469 exename = exename ~ ".exe"; 470 } 471 // TODO: use targetPath 472 string exePath = buildNormalizedPath(_filename.dirName, "bin", exename); 473 return exePath; 474 } 475 476 /// working directory for running and debugging project 477 @property string workingDirectory() { 478 // TODO: get from settings 479 return _filename.dirName; 480 } 481 482 /// commandline parameters for running and debugging project 483 @property string runArgs() { 484 // TODO: get from settings 485 return null; 486 } 487 488 @property bool runInExternalConsole() { 489 // TODO 490 return true; 491 } 492 493 ProjectFolder findItems(string[] srcPaths) { 494 ProjectFolder folder = new ProjectFolder(_filename); 495 folder.project = this; 496 string path = relativeToAbsolutePath("src"); 497 if (folder.loadDir(path)) 498 _sourcePaths ~= path; 499 path = relativeToAbsolutePath("source"); 500 if (folder.loadDir(path)) 501 _sourcePaths ~= path; 502 foreach(customPath; srcPaths) { 503 path = relativeToAbsolutePath(customPath); 504 foreach(existing; _sourcePaths) 505 if (path.equal(existing)) 506 continue; // already exists 507 if (folder.loadDir(path)) 508 _sourcePaths ~= path; 509 } 510 return folder; 511 } 512 513 void refresh() { 514 for (int i = _items._children.count - 1; i >= 0; i--) { 515 if (_items._children[i].isFolder) 516 _items._children[i].refresh(); 517 } 518 } 519 520 void findMainSourceFile() { 521 string n = toUTF8(name); 522 string[] mainnames = ["app.d", "main.d", n ~ ".d"]; 523 foreach(sname; mainnames) { 524 _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "src", sname)); 525 if (_mainSourceFile) 526 break; 527 _mainSourceFile = findSourceFileItem(buildNormalizedPath(_dir, "source", sname)); 528 if (_mainSourceFile) 529 break; 530 } 531 } 532 533 /// tries to find source file in project, returns found project source file item, or null if not found 534 ProjectSourceFile findSourceFileItem(ProjectItem dir, string filename, bool fullFileName=true) { 535 for (int i = 0; i < dir.childCount; i++) { 536 ProjectItem item = dir.child(i); 537 if (item.isFolder) { 538 ProjectSourceFile res = findSourceFileItem(item, filename, fullFileName); 539 if (res) 540 return res; 541 } else { 542 ProjectSourceFile res = cast(ProjectSourceFile)item; 543 if(res) 544 { 545 if(fullFileName && res.filename.equal(filename)) 546 return res; 547 else if (!fullFileName && res.filename.endsWith(filename)) 548 return res; 549 } 550 } 551 } 552 return null; 553 } 554 555 ProjectSourceFile findSourceFileItem(string filename, bool fullFileName=true) { 556 return findSourceFileItem(_items, filename, fullFileName); 557 } 558 559 override bool load(string fname = null) { 560 if (!_projectFile) 561 _projectFile = new SettingsFile(); 562 _mainSourceFile = null; 563 if (fname.length > 0) 564 filename = fname; 565 if (!_projectFile.load(_filename)) { 566 Log.e("failed to load project from file ", _filename); 567 return false; 568 } 569 Log.d("Reading project from file ", _filename); 570 571 try { 572 _name = toUTF32(_projectFile.getString("name")); 573 if (_isDependency) { 574 _name ~= "-"d; 575 _name ~= toUTF32(_dependencyVersion.startsWith("~") ? _dependencyVersion[1..$] : _dependencyVersion); 576 } 577 _description = toUTF32(_projectFile.getString("description")); 578 Log.d(" project name: ", _name); 579 Log.d(" project description: ", _description); 580 string[] srcPaths = _projectFile.getStringArray("sourcePaths"); 581 _items = findItems(srcPaths); 582 findMainSourceFile(); 583 584 Log.i("Project source paths: ", sourcePaths); 585 Log.i("Builder source paths: ", builderSourcePaths); 586 if (!_isDependency) 587 loadSelections(); 588 589 _configurations = ProjectConfiguration.load(_projectFile); 590 Log.i("Project configurations: ", _configurations); 591 592 } catch (JSONException e) { 593 Log.e("Cannot parse json", e); 594 return false; 595 } catch (Exception e) { 596 Log.e("Cannot read project file", e); 597 return false; 598 } 599 return true; 600 } 601 602 override bool save(string fname = null) { 603 if (fname !is null) 604 filename = fname; 605 assert(filename !is null); 606 return _projectFile.save(filename, true); 607 } 608 609 protected Project[] _dependencies; 610 @property Project[] dependencies() { return _dependencies; } 611 protected bool addDependency(Project dep) { 612 if (_workspace) 613 _workspace.addDependencyProject(dep); 614 _dependencies ~= dep; 615 return true; 616 } 617 bool loadSelections() { 618 _dependencies.length = 0; 619 DubPackageFinder finder = new DubPackageFinder(); 620 scope(exit) destroy(finder); 621 SettingsFile selectionsFile = new SettingsFile(buildNormalizedPath(_dir, "dub.selections.json")); 622 if (!selectionsFile.load()) 623 return false; 624 Setting versions = selectionsFile.objectByPath("versions"); 625 if (!versions.isObject) 626 return false; 627 string[string] versionMap = versions.strMap; 628 foreach(packageName, packageVersion; versionMap) { 629 string fn = finder.findPackage(packageName, packageVersion); 630 Log.d("dependency ", packageName, " ", packageVersion, " : ", fn ? fn : "NOT FOUND"); 631 if (fn) { 632 Project p = new Project(_workspace, fn, packageVersion); 633 if (p.load()) { 634 addDependency(p); 635 } else { 636 Log.e("cannot load dependency package ", packageName, " ", packageVersion, " from file ", fn); 637 destroy(p); 638 } 639 } 640 } 641 return true; 642 } 643 } 644 645 class DubPackageFinder { 646 string systemDubPath; 647 string userDubPath; 648 string tempPath; 649 this() { 650 version(Windows){ 651 systemDubPath = buildNormalizedPath(environment.get("ProgramData"), "dub", "packages"); 652 userDubPath = buildNormalizedPath(environment.get("APPDATA"), "dub", "packages"); 653 tempPath = buildNormalizedPath(environment.get("TEMP"), "dub", "packages"); 654 } else version(Posix){ 655 systemDubPath = "/var/lib/dub/packages"; 656 userDubPath = buildNormalizedPath(environment.get("HOME"), ".dub", "packages"); 657 if(!userDubPath.isAbsolute) 658 userDubPath = buildNormalizedPath(getcwd(), userDubPath); 659 tempPath = "/tmp/packages"; 660 } 661 } 662 663 protected string findPackage(string packageDir, string packageName, string packageVersion) { 664 string fullName = packageVersion.startsWith("~") ? packageName ~ "-" ~ packageVersion[1..$] : packageName ~ "-" ~ packageVersion; 665 string pathName = absolutePath(buildNormalizedPath(packageDir, fullName)); 666 if (pathName.exists && pathName.isDir) { 667 string fn = buildNormalizedPath(pathName, "dub.json"); 668 if (fn.exists && fn.isFile) 669 return fn; 670 fn = buildNormalizedPath(pathName, "package.json"); 671 if (fn.exists && fn.isFile) 672 return fn; 673 } 674 return null; 675 } 676 677 string findPackage(string packageName, string packageVersion) { 678 string res = null; 679 res = findPackage(userDubPath, packageName, packageVersion); 680 if (res) 681 return res; 682 res = findPackage(systemDubPath, packageName, packageVersion); 683 return res; 684 } 685 } 686 687 bool isValidProjectName(string s) { 688 if (s.empty) 689 return false; 690 for (int i = 0; i < s.length; i++) { 691 char ch = s[i]; 692 if (ch != '_' && ch != '-' && (ch < '0' || ch > '9') && (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z')) 693 return false; 694 } 695 return true; 696 } 697 698 bool isValidModuleName(string s) { 699 if (s.empty) 700 return false; 701 for (int i = 0; i < s.length; i++) { 702 char ch = s[i]; 703 if (ch != '_' && (ch < '0' || ch > '9') && (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z')) 704 return false; 705 } 706 return true; 707 } 708 709 bool isValidFileName(string s) { 710 if (s.empty) 711 return false; 712 for (int i = 0; i < s.length; i++) { 713 char ch = s[i]; 714 if (ch != '_' && ch != '.' && ch != '-' && (ch < '0' || ch > '9') && (ch < 'a' || ch > 'z') && (ch < 'A' || ch > 'Z')) 715 return false; 716 } 717 return true; 718 }