1 /**
2 	...
3 
4 	Copyright: © 2012 Matthias Dondorff
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff
7 */
8 module dub.internal.utils;
9 
10 import dub.internal.vibecompat.core.file;
11 import dub.internal.vibecompat.core.log;
12 import dub.internal.vibecompat.data.json;
13 import dub.internal.vibecompat.inet.url;
14 import dub.compilers.buildsettings : BuildSettings;
15 import dub.version_;
16 
17 import core.time : Duration;
18 import std.algorithm : canFind, startsWith;
19 import std.array : appender, array;
20 import std.conv : to;
21 import std.exception : enforce;
22 import std.file;
23 import std.string : format;
24 import std.process;
25 import std.traits : isIntegral;
26 version(DubUseCurl)
27 {
28 	import std.net.curl;
29 	public import std.net.curl : HTTPStatusException;
30 }
31 
32 
33 private NativePath[] temporary_files;
34 
35 NativePath getTempDir()
36 {
37 	return NativePath(std.file.tempDir());
38 }
39 
40 NativePath getTempFile(string prefix, string extension = null)
41 {
42 	import std.uuid : randomUUID;
43 	import std.array: replace;
44 
45 	string fileName = prefix ~ "-" ~ randomUUID.toString() ~ extension;
46 
47 	if (extension !is null && extension == ".d")
48 		fileName = fileName.replace("-", "_");
49 
50 	auto path = getTempDir() ~ fileName;
51 	temporary_files ~= path;
52 	return path;
53 }
54 
55 /**
56    Obtain a lock for a file at the given path. If the file cannot be locked
57    within the given duration, an exception is thrown.  The file will be created
58    if it does not yet exist. Deleting the file is not safe as another process
59    could create a new file with the same name.
60    The returned lock will get unlocked upon destruction.
61 
62    Params:
63      path = path to file that gets locked
64      timeout = duration after which locking failed
65    Returns:
66      The locked file or an Exception on timeout.
67 */
68 auto lockFile(string path, Duration timeout)
69 {
70 	import core.thread : Thread;
71 	import std.datetime, std.stdio : File;
72 	import std.algorithm : move;
73 
74 	// Just a wrapper to hide (and destruct) the locked File.
75 	static struct LockFile
76 	{
77 		// The Lock can't be unlinked as someone could try to lock an already
78 		// opened fd while a new file with the same name gets created.
79 		// Exclusive filesystem locks (O_EXCL, mkdir) could be deleted but
80 		// aren't automatically freed when a process terminates, see #1149.
81 		private File f;
82 	}
83 
84 	auto file = File(path, "w");
85 	auto t0 = Clock.currTime();
86 	auto dur = 1.msecs;
87 	while (true)
88 	{
89 		if (file.tryLock())
90 			return LockFile(move(file));
91 		enforce(Clock.currTime() - t0 < timeout, "Failed to lock '"~path~"'.");
92 		if (dur < 1024.msecs) // exponentially increase sleep time
93 			dur *= 2;
94 		Thread.sleep(dur);
95 	}
96 }
97 
98 static ~this()
99 {
100 	foreach (path; temporary_files)
101 	{
102 		auto spath = path.toNativeString();
103 		if (spath.exists)
104 			std.file.remove(spath);
105 	}
106 }
107 
108 bool isWritableDir(NativePath p, bool create_if_missing = false)
109 {
110 	import std.random;
111 	auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max));
112 	if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString());
113 	try openFile(fname, FileMode.createTrunc).close();
114 	catch (Exception) return false;
115 	remove(fname.toNativeString());
116 	return true;
117 }
118 
119 Json jsonFromFile(NativePath file, bool silent_fail = false) {
120 	if( silent_fail && !existsFile(file) ) return Json.emptyObject;
121 	auto f = openFile(file.toNativeString(), FileMode.read);
122 	scope(exit) f.close();
123 	auto text = stripUTF8Bom(cast(string)f.readAll());
124 	return parseJsonString(text, file.toNativeString());
125 }
126 
127 /**
128 	Read package info file content from archive.
129 	File needs to be in root folder or in first
130 	sub folder.
131 
132 	Params:
133 		zip = path to archive file
134 		fileName = Package file name
135 	Returns:
136 		package file content.
137 */
138 string packageInfoFileFromZip(NativePath zip, out string fileName) {
139 	import std.zip : ZipArchive, ArchiveMember;
140 	import dub.package_ : packageInfoFiles;
141 
142 	auto f = openFile(zip, FileMode.read);
143 	ubyte[] b = new ubyte[cast(size_t)f.size];
144 	f.rawRead(b);
145 	f.close();
146 	auto archive = new ZipArchive(b);
147 	alias PSegment = typeof (NativePath.init.head);
148 	foreach (ArchiveMember am; archive.directory) {
149 		auto path = NativePath(am.name).bySegment.array;
150 		foreach (fil; packageInfoFiles) {
151 			if ((path.length == 1 && path[0] == fil.filename) || (path.length == 2 && path[$-1].name == fil.filename)) {
152 				fileName = fil.filename;
153 				return stripUTF8Bom(cast(string) archive.expand(archive.directory[am.name]));
154 			}
155 		}
156 	}
157 	throw new Exception("No package descriptor found");
158 }
159 
160 void writeJsonFile(NativePath path, Json json)
161 {
162 	auto f = openFile(path, FileMode.createTrunc);
163 	scope(exit) f.close();
164 	f.writePrettyJsonString(json);
165 }
166 
167 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file
168 void atomicWriteJsonFile(NativePath path, Json json)
169 {
170 	import std.random : uniform;
171 	auto tmppath = path.parentPath ~ format("%s.%s.tmp", path.head, uniform(0, int.max));
172 	auto f = openFile(tmppath, FileMode.createTrunc);
173 	scope (failure) {
174 		f.close();
175 		removeFile(tmppath);
176 	}
177 	f.writePrettyJsonString(json);
178 	f.close();
179 	if (existsFile(path)) removeFile(path);
180 	moveFile(tmppath, path);
181 }
182 
183 bool existsDirectory(NativePath path) {
184 	if( !existsFile(path) ) return false;
185 	auto fi = getFileInfo(path);
186 	return fi.isDirectory;
187 }
188 
189 void runCommand(string command, string[string] env = null, string workDir = null)
190 {
191 	runCommands((&command)[0 .. 1], env, workDir);
192 }
193 
194 void runCommands(in string[] commands, string[string] env = null, string workDir = null)
195 {
196 	import std.stdio : stdin, stdout, stderr, File;
197 
198 	version(Windows) enum nullFile = "NUL";
199 	else version(Posix) enum nullFile = "/dev/null";
200 	else static assert(0);
201 
202 	auto childStdout = stdout;
203 	auto childStderr = stderr;
204 	auto config = Config.retainStdout | Config.retainStderr;
205 
206 	// Disable child's stdout/stderr depending on LogLevel
207 	auto logLevel = getLogLevel();
208 	if(logLevel >= LogLevel.warn)
209 		childStdout = File(nullFile, "w");
210 	if(logLevel >= LogLevel.none)
211 		childStderr = File(nullFile, "w");
212 
213 	foreach(cmd; commands){
214 		logDiagnostic("Running %s", cmd);
215 		Pid pid;
216 		pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config, workDir);
217 		auto exitcode = pid.wait();
218 		enforce(exitcode == 0, "Command failed with exit code "
219 			~ to!string(exitcode) ~ ": " ~ cmd);
220 	}
221 }
222 
223 version (Have_vibe_d_http)
224 	public import vibe.http.common : HTTPStatusException;
225 
226 /**
227 	Downloads a file from the specified URL.
228 
229 	Any redirects will be followed until the actual file resource is reached or if the redirection
230 	limit of 10 is reached. Note that only HTTP(S) is currently supported.
231 
232 	The download times out if a connection cannot be established within
233 	`timeout` ms, or if the average transfer rate drops below 10 bytes / s for
234 	more than `timeout` seconds.  Pass `0` as `timeout` to disable both timeout
235 	mechanisms.
236 
237 	Note: Timeouts are only implemented when curl is used (DubUseCurl).
238 */
239 void download(string url, string filename, uint timeout = 8)
240 {
241 	version(DubUseCurl) {
242 		auto conn = HTTP();
243 		setupHTTPClient(conn, timeout);
244 		logDebug("Storing %s...", url);
245 		std.net.curl.download(url, filename, conn);
246 		// workaround https://issues.dlang.org/show_bug.cgi?id=18318
247 		auto sl = conn.statusLine;
248 		logDebug("Download %s %s", url, sl);
249 		if (sl.code / 100 != 2)
250 			throw new HTTPStatusException(sl.code,
251 				"Downloading %s failed with %d (%s).".format(url, sl.code, sl.reason));
252 	} else version (Have_vibe_d_http) {
253 		import vibe.inet.urltransfer;
254 		vibe.inet.urltransfer.download(url, filename);
255 	} else assert(false);
256 }
257 /// ditto
258 void download(URL url, NativePath filename, uint timeout = 8)
259 {
260 	download(url.toString(), filename.toNativeString(), timeout);
261 }
262 /// ditto
263 ubyte[] download(string url, uint timeout = 8)
264 {
265 	version(DubUseCurl) {
266 		auto conn = HTTP();
267 		setupHTTPClient(conn, timeout);
268 		logDebug("Getting %s...", url);
269 		return cast(ubyte[])get(url, conn);
270 	} else version (Have_vibe_d_http) {
271 		import vibe.inet.urltransfer;
272 		import vibe.stream.operations;
273 		ubyte[] ret;
274 		vibe.inet.urltransfer.download(url, (scope input) { ret = input.readAll(); });
275 		return ret;
276 	} else assert(false);
277 }
278 /// ditto
279 ubyte[] download(URL url, uint timeout = 8)
280 {
281 	return download(url.toString(), timeout);
282 }
283 
284 /**
285 	Downloads a file from the specified URL with retry logic.
286 
287 	Downloads a file from the specified URL with up to n tries on failure
288 	Throws: `Exception` if the download failed or `HTTPStatusException` after the nth retry or
289 	on "unrecoverable failures" such as 404 not found
290 	Otherwise might throw anything else that `download` throws.
291 	See_Also: download
292 
293 	The download times out if a connection cannot be established within
294 	`timeout` ms, or if the average transfer rate drops below 10 bytes / s for
295 	more than `timeout` seconds.  Pass `0` as `timeout` to disable both timeout
296 	mechanisms.
297 
298 	Note: Timeouts are only implemented when curl is used (DubUseCurl).
299 **/
300 void retryDownload(URL url, NativePath filename, size_t retryCount = 3, uint timeout = 8)
301 {
302 	foreach(i; 0..retryCount) {
303 		version(DubUseCurl) {
304 			try {
305 				download(url, filename, timeout);
306 				return;
307 			}
308 			catch(HTTPStatusException e) {
309 				if (e.status == 404) throw e;
310 				else {
311 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
312 					if (i == retryCount - 1) throw e;
313 					else continue;
314 				}
315 			}
316 			catch(CurlException e) {
317 				logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
318 				continue;
319 			}
320 		}
321 		else
322 		{
323 			try {
324 				download(url, filename);
325 				return;
326 			}
327 			catch(HTTPStatusException e) {
328 				if (e.status == 404) throw e;
329 				else {
330 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
331 					if (i == retryCount - 1) throw e;
332 					else continue;
333 				}
334 			}
335 		}
336 	}
337 	throw new Exception("Failed to download %s".format(url));
338 }
339 
340 ///ditto
341 ubyte[] retryDownload(URL url, size_t retryCount = 3, uint timeout = 8)
342 {
343 	foreach(i; 0..retryCount) {
344 		version(DubUseCurl) {
345 			try {
346 				return download(url, timeout);
347 			}
348 			catch(HTTPStatusException e) {
349 				if (e.status == 404) throw e;
350 				else {
351 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
352 					if (i == retryCount - 1) throw e;
353 					else continue;
354 				}
355 			}
356 			catch(CurlException e) {
357 				logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
358 				continue;
359 			}
360 		}
361 		else
362 		{
363 			try {
364 				return download(url);
365 			}
366 			catch(HTTPStatusException e) {
367 				if (e.status == 404) throw e;
368 				else {
369 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
370 					if (i == retryCount - 1) throw e;
371 					else continue;
372 				}
373 			}
374 		}
375 	}
376 	throw new Exception("Failed to download %s".format(url));
377 }
378 
379 /// Returns the current DUB version in semantic version format
380 string getDUBVersion()
381 {
382 	import dub.version_;
383 	import std.array : split, join;
384 	// convert version string to valid SemVer format
385 	auto verstr = dubVersion;
386 	if (verstr.startsWith("v")) verstr = verstr[1 .. $];
387 	auto parts = verstr.split("-");
388 	if (parts.length >= 3) {
389 		// detect GIT commit suffix
390 		if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber())
391 			verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-");
392 	}
393 	return verstr;
394 }
395 
396 
397 /**
398 	Get current executable's path if running as DUB executable,
399 	or find a DUB executable if DUB is used as a library.
400 	For the latter, the following locations are checked in order:
401 	$(UL
402 		$(LI current working directory)
403 		$(LI same directory as `compilerBinary` (if supplied))
404 		$(LI all components of the `$PATH` variable)
405 	)
406 	Params:
407 		compilerBinary = optional path to a D compiler executable, used to locate DUB executable
408 	Returns:
409 		The path to a valid DUB executable
410 	Throws:
411 		an Exception if no valid DUB executable is found
412 */
413 public string getDUBExePath(in string compilerBinary=null)
414 {
415 	version(DubApplication) {
416 		import std.file : thisExePath;
417 		return thisExePath();
418 	}
419 	else {
420 		// this must be dub as a library
421 		import std.algorithm : filter, map, splitter;
422 		import std.array : array;
423 		import std.file : exists, getcwd;
424 		import std.path : chainPath, dirName;
425 		import std.range : chain, only, take;
426 		import std.process : environment;
427 
428 		version(Windows) {
429 			enum exeName = "dub.exe";
430 			enum pathSep = ';';
431 		}
432 		else {
433 			enum exeName = "dub";
434 			enum pathSep = ':';
435 		}
436 
437 		auto dubLocs = only(
438 			getcwd().chainPath(exeName),
439 			compilerBinary.dirName.chainPath(exeName),
440 		)
441 		.take(compilerBinary.length ? 2 : 1)
442 		.chain(
443 			environment.get("PATH", "")
444 				.splitter(pathSep)
445 				.map!(p => p.chainPath(exeName))
446 		)
447 		.filter!exists;
448 
449 		enforce(!dubLocs.empty, "Could not find DUB executable");
450 		return dubLocs.front.array;
451 	}
452 }
453 
454 
455 version(DubUseCurl) {
456 	void setupHTTPClient(ref HTTP conn, uint timeout)
457 	{
458 		static if( is(typeof(&conn.verifyPeer)) )
459 			conn.verifyPeer = false;
460 
461 		auto proxy = environment.get("http_proxy", null);
462 		if (proxy.length) conn.proxy = proxy;
463 
464 		auto noProxy = environment.get("no_proxy", null);
465 		if (noProxy.length) conn.handle.set(CurlOption.noproxy, noProxy);
466 
467 		conn.handle.set(CurlOption.encoding, "");
468 		if (timeout) {
469 			// connection (TLS+TCP) times out after 8s
470 			conn.handle.set(CurlOption.connecttimeout, timeout);
471 			// transfers time out after 8s below 10 byte/s
472 			conn.handle.set(CurlOption.low_speed_limit, 10);
473 			conn.handle.set(CurlOption.low_speed_time, timeout);
474 		}
475 
476 		conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)");
477 	}
478 }
479 
480 string stripUTF8Bom(string str)
481 {
482 	if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] )
483 		return str[3 ..$];
484 	return str;
485 }
486 
487 private bool isNumber(string str) {
488 	foreach (ch; str)
489 		switch (ch) {
490 			case '0': .. case '9': break;
491 			default: return false;
492 		}
493 	return true;
494 }
495 
496 private bool isHexNumber(string str) {
497 	foreach (ch; str)
498 		switch (ch) {
499 			case '0': .. case '9': break;
500 			case 'a': .. case 'f': break;
501 			case 'A': .. case 'F': break;
502 			default: return false;
503 		}
504 	return true;
505 }
506 
507 /**
508 	Get the closest match of $(D input) in the $(D array), where $(D distance)
509 	is the maximum levenshtein distance allowed between the compared strings.
510 	Returns $(D null) if no closest match is found.
511 */
512 string getClosestMatch(string[] array, string input, size_t distance)
513 {
514 	import std.algorithm : countUntil, map, levenshteinDistance;
515 	import std.uni : toUpper;
516 
517 	auto distMap = array.map!(elem =>
518 		levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input));
519 	auto idx = distMap.countUntil!(a => a <= distance);
520 	return (idx == -1) ? null : array[idx];
521 }
522 
523 /**
524 	Searches for close matches to input in range. R must be a range of strings
525 	Note: Sorts the strings range. Use std.range.indexed to avoid this...
526   */
527 auto fuzzySearch(R)(R strings, string input){
528 	import std.algorithm : levenshteinDistance, schwartzSort, partition3;
529 	import std.traits : isSomeString;
530 	import std.range : ElementType;
531 
532 	static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang");
533 	immutable threshold = input.length / 4;
534 	return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1]
535 			.schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper));
536 }
537 
538 /**
539 	If T is a bitfield-style enum, this function returns a string range
540 	listing the names of all members included in the given value.
541 
542 	Example:
543 	---------
544 	enum Bits {
545 		none = 0,
546 		a = 1<<0,
547 		b = 1<<1,
548 		c = 1<<2,
549 		a_c = a | c,
550 	}
551 
552 	assert( bitFieldNames(Bits.none).equals(["none"]) );
553 	assert( bitFieldNames(Bits.a).equals(["a"]) );
554 	assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) );
555 	---------
556   */
557 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T)
558 {
559 	import std.algorithm : filter, map;
560 	import std.conv : to;
561 	import std.traits : EnumMembers;
562 
563 	return [ EnumMembers!(T) ]
564 		.filter!(member => member==0? value==0 : (value & member) == member)
565 		.map!(member => to!string(member));
566 }
567 
568 
569 bool isIdentChar(dchar ch)
570 {
571 	import std.ascii : isAlphaNum;
572 	return isAlphaNum(ch) || ch == '_';
573 }
574 
575 string stripDlangSpecialChars(string s)
576 {
577 	import std.array : appender;
578 	auto ret = appender!string();
579 	foreach(ch; s)
580 		ret.put(isIdentChar(ch) ? ch : '_');
581 	return ret.data;
582 }
583 
584 string determineModuleName(BuildSettings settings, NativePath file, NativePath base_path)
585 {
586 	import std.algorithm : map;
587 	import std.array : array;
588 	import std.range : walkLength;
589 
590 	assert(base_path.absolute);
591 	if (!file.absolute) file = base_path ~ file;
592 
593 	size_t path_skip = 0;
594 	foreach (ipath; settings.importPaths.map!(p => NativePath(p))) {
595 		if (!ipath.absolute) ipath = base_path ~ ipath;
596 		assert(!ipath.empty);
597 		if (file.startsWith(ipath) && ipath.bySegment.walkLength > path_skip)
598 			path_skip = ipath.bySegment.walkLength;
599 	}
600 
601 	enforce(path_skip > 0,
602 		format("Source file '%s' not found in any import path.", file.toNativeString()));
603 
604 	auto mpath = file.bySegment.array[path_skip .. $];
605 	auto ret = appender!string;
606 
607 	//search for module keyword in file
608 	string moduleName = getModuleNameFromFile(file.to!string);
609 
610 	if(moduleName.length) return moduleName;
611 
612 	//create module name from path
613 	foreach (i; 0 .. mpath.length) {
614 		import std.path;
615 		auto p = mpath[i].name;
616 		if (p == "package.d") break;
617 		if (i > 0) ret ~= ".";
618 		if (i+1 < mpath.length) ret ~= p;
619 		else ret ~= p.baseName(".d");
620 	}
621 
622 	return ret.data;
623 }
624 
625 /**
626  * Search for module keyword in D Code
627  */
628 string getModuleNameFromContent(string content) {
629 	import std.regex;
630 	import std.string;
631 
632 	content = content.strip;
633 	if (!content.length) return null;
634 
635 	static bool regex_initialized = false;
636 	static Regex!char comments_pattern, module_pattern;
637 
638 	if (!regex_initialized) {
639 		comments_pattern = regex(`//[^\r\n]*\r?\n?|/\*.*?\*/|/\+.*\+/`, "gs");
640 		module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g");
641 		regex_initialized = true;
642 	}
643 
644 	content = replaceAll(content, comments_pattern, " ");
645 	auto result = matchFirst(content, module_pattern);
646 
647 	if (!result.empty) return result[1];
648 
649 	return null;
650 }
651 
652 unittest {
653 	assert(getModuleNameFromContent("") == "");
654 	assert(getModuleNameFromContent("module myPackage.myModule;") == "myPackage.myModule");
655 	assert(getModuleNameFromContent("module \t\n myPackage.myModule \t\r\n;") == "myPackage.myModule");
656 	assert(getModuleNameFromContent("// foo\nmodule bar;") == "bar");
657 	assert(getModuleNameFromContent("/*\nfoo\n*/\nmodule bar;") == "bar");
658 	assert(getModuleNameFromContent("/+\nfoo\n+/\nmodule bar;") == "bar");
659 	assert(getModuleNameFromContent("/***\nfoo\n***/\nmodule bar;") == "bar");
660 	assert(getModuleNameFromContent("/+++\nfoo\n+++/\nmodule bar;") == "bar");
661 	assert(getModuleNameFromContent("// module foo;\nmodule bar;") == "bar");
662 	assert(getModuleNameFromContent("/* module foo; */\nmodule bar;") == "bar");
663 	assert(getModuleNameFromContent("/+ module foo; +/\nmodule bar;") == "bar");
664 	assert(getModuleNameFromContent("/+ /+ module foo; +/ +/\nmodule bar;") == "bar");
665 	assert(getModuleNameFromContent("// module foo;\nmodule bar; // module foo;") == "bar");
666 	assert(getModuleNameFromContent("// module foo;\nmodule// module foo;\nbar//module foo;\n;// module foo;") == "bar");
667 	assert(getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;") == "bar", getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;"));
668 	assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar;") == "bar");
669 	//assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar/++/;") == "bar"); // nested comments require a context-free parser!
670 	assert(getModuleNameFromContent("/*\nmodule sometest;\n*/\n\nmodule fakemath;\n") == "fakemath");
671 }
672 
673 /**
674  * Search for module keyword in file
675  */
676 string getModuleNameFromFile(string filePath) {
677 	string fileContent = filePath.readText;
678 
679 	logDiagnostic("Get module name from path: " ~ filePath);
680 	return getModuleNameFromContent(fileContent);
681 }