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 }