1 module served.utils.stdlib_detect;
2 
3 import std.algorithm : countUntil, splitter, startsWith;
4 import std.array : appender, replace;
5 import std.ascii : isWhite;
6 import std.conv : to;
7 import std.experimental.logger : trace, warning;
8 import std.path : baseName, buildNormalizedPath, buildPath, chainPath, dirName, isAbsolute, stripExtension;
9 import std.process : environment;
10 import std.string : endsWith, indexOf, startsWith, strip, stripLeft;
11 import std.uni : sicmp;
12 import std.utf : decode, UseReplacementDchar;
13 
14 import fs = std.file;
15 import io = std.stdio;
16 
17 string[] autoDetectStdlibPaths(string cwd = null, string compilerPath = null)
18 {
19 	string[] ret;
20 
21 	if (compilerPath.length && !isAbsolute(compilerPath))
22 		compilerPath = searchPathFor(compilerPath);
23 
24 	if (compilerPath.length)
25 	{
26 		auto binName = compilerPath.baseName.stripExtension;
27 		switch (binName)
28 		{
29 		case "dmd":
30 			trace("detecting dmd stdlib path from ", compilerPath);
31 			if (detectDMDStdlibPaths(cwd, ret, compilerPath))
32 				return ret;
33 			break;
34 		case "ldc":
35 		case "ldc2":
36 			trace("detecting ldc stdlib path from ", compilerPath);
37 			if (detectLDCStdlibPaths(cwd, ret, compilerPath))
38 				return ret;
39 			break;
40 		case "gdc":
41 		case "gcc":
42 			trace("detecting gdc stdlib path from ", compilerPath);
43 			warning("\"d.stdlibPath\" set to \"auto\", but gdc/gcc (as set by d.dubCompiler) is not supported for auto detection, falling back to dmd or ldc");
44 			break;
45 		default:
46 			warning("\"d.stdlibPath\" set to \"auto\", but I don't know what the dubCompiler is by checking the filename, falling back to dmd or ldc");
47 			break;
48 		}
49 	}
50 
51 	trace("falling back to global imports search");
52 	if (detectDMDStdlibPaths(cwd, ret) || detectLDCStdlibPaths(cwd, ret))
53 	{
54 		trace("found stdlib paths in DMD or LDC: ", ret);
55 		return ret;
56 	}
57 	else
58 	{
59 		warning("returning to default hardcoded fallback phobos paths");
60 		version (Windows)
61 			return [`C:\D\dmd2\src\druntime\import`, `C:\D\dmd2\src\phobos`];
62 		else version (OSX)
63 			return [`/Library/D/dmd/src/druntime/import`, `/Library/D/dmd/src/phobos`];
64 		else version (Posix)
65 			return [`/usr/include/dmd/druntime/import`, `/usr/include/dmd/phobos`];
66 		else
67 		{
68 			pragma(msg, __FILE__ ~ "(" ~ __LINE__
69 					~ "): Note: Unknown target OS. Please add default D stdlib path");
70 			return [];
71 		}
72 	}
73 }
74 
75 bool detectDMDStdlibPaths(string cwd, out string[] ret, string dmdPath = null)
76 {
77 	// https://dlang.org/dmd-linux.html#dmd-conf
78 	// https://dlang.org/dmd-osx.html#dmd-conf
79 	// https://dlang.org/dmd-windows.html#sc-ini
80 
81 	version (Windows)
82 	{
83 		static immutable confName = "sc.ini";
84 		static immutable dmdExe = "dmd.exe";
85 	}
86 	else
87 	{
88 		static immutable confName = "dmd.conf";
89 		static immutable dmdExe = "dmd";
90 	}
91 
92 	if (cwd.length && fs.exists(chainPath(cwd, confName))
93 			&& parseDmdConfImports(buildPath(cwd, confName), cwd, ret))
94 		return true;
95 
96 	string home = environment.get("HOME");
97 	if (home.length && fs.exists(chainPath(home, confName))
98 			&& parseDmdConfImports(buildPath(home, confName), home, ret))
99 		return true;
100 
101 	if (!dmdPath.length || !fs.exists(dmdPath))
102 		dmdPath = searchPathFor(dmdExe);
103 
104 	if (dmdPath.length)
105 	{
106 		auto dmdDir = dirName(dmdPath);
107 		if (fs.exists(chainPath(dmdDir, confName))
108 				&& parseDmdConfImports(buildPath(dmdDir, confName), dmdDir, ret))
109 			return true;
110 	}
111 	else
112 	{
113 		warning("Could not find DMD in $PATH for stdlib auto-detection! ",
114 			"Checking for dmd.conf at hardcoded system-wide location...");
115 	}
116 
117 	version (Windows)
118 	{
119 		if (dmdPath.length)
120 		{
121 			auto dmdDir = dirName(dmdPath);
122 			bool haveDRuntime = fs.exists(chainPath(dmdDir, "..", "..", "src",
123 					"druntime", "import"));
124 			bool havePhobos = fs.exists(chainPath(dmdDir, "..", "..", "src", "phobos"));
125 			if (haveDRuntime && havePhobos)
126 				ret = [
127 					buildNormalizedPath(dmdDir, "..", "..", "src", "druntime",
128 							"import"),
129 					buildNormalizedPath(dmdDir, "..", "..", "src", "phobos")
130 				];
131 			else if (haveDRuntime)
132 				ret = [
133 					buildNormalizedPath(dmdDir, "..", "..", "src", "druntime", "import")
134 				];
135 			else if (havePhobos)
136 				ret = [buildNormalizedPath(dmdDir, "..", "..", "src", "phobos")];
137 
138 			return ret.length > 0;
139 		}
140 		else
141 		{
142 			return false;
143 		}
144 	}
145 	else version (Posix)
146 	{
147 		if (fs.exists("/etc/dmd.conf") && parseDmdConfImports("/etc/dmd.conf", "/etc", ret))
148 			return true;
149 
150 		if (fs.exists("/usr/local/etc/dmd.conf") && parseDmdConfImports("/usr/local/etc/dmd.conf", "/usr/local/etc", ret))
151 			return true;
152 
153 		return false;
154 	}
155 	else
156 	{
157 		pragma(msg,
158 				__FILE__ ~ "(" ~ __LINE__
159 				~ "): Note: Unknown target OS. Please add default dmd stdlib path");
160 		return false;
161 	}
162 }
163 
164 bool detectLDCStdlibPaths(string cwd, out string[] ret, string ldcPath = null)
165 {
166 	// https://github.com/ldc-developers/ldc/blob/829dc71114eaf7c769208f03eb9a614dafd789c3/driver/configfile.cpp
167 
168 	static bool tryPath(R)(R path, lazy scope const(char)[] pathDir, out string[] ret)
169 	{
170 		return fs.exists(path) && parseLdcConfImports(path.to!string, pathDir, ret);
171 	}
172 
173 	static immutable confName = "ldc2.conf";
174 	version (Windows)
175 	{
176 		static immutable ldcExe = "ldc2.exe";
177 		static immutable ldcExeAlt = "ldc.exe";
178 	}
179 	else
180 	{
181 		static immutable ldcExe = "ldc2";
182 		static immutable ldcExeAlt = "ldc";
183 	}
184 
185 	if (!ldcPath.length || !fs.exists(ldcPath))
186 		ldcPath = searchPathFor(ldcExe);
187 
188 	if (!ldcPath.length)
189 		ldcPath = searchPathFor(ldcExeAlt);
190 	auto ldcDir = ldcPath.length ? dirName(ldcPath) : null;
191 
192 	if (tryPath(chainPath(cwd, confName), ldcDir, ret))
193 		return true;
194 
195 	if (ldcPath.length)
196 	{
197 		if (tryPath(chainPath(ldcDir, confName), ldcDir, ret))
198 			return true;
199 	}
200 
201 	string home = getUserHomeDirectoryLDC();
202 	if (home.length)
203 	{
204 		if (tryPath(chainPath(home, ".ldc", confName), ldcDir, ret))
205 			return true;
206 
207 		version (Windows)
208 		{
209 			if (tryPath(chainPath(home, confName), ldcDir, ret))
210 				return true;
211 		}
212 	}
213 
214 	if (ldcPath.length)
215 	{
216 		if (tryPath(chainPath(ldcDir.dirName, "etc", confName), ldcDir, ret))
217 			return true;
218 	}
219 
220 	version (Windows)
221 	{
222 		string path = readLdcPathFromRegistry();
223 		if (path.length && tryPath(chainPath(path, "etc", confName), ldcDir, ret))
224 			return true;
225 	}
226 	else
227 	{
228 		if (tryPath(chainPath("/etc", confName), ldcDir, ret))
229 			return true;
230 		if (tryPath(chainPath("/etc/ldc", confName), ldcDir, ret))
231 			return true;
232 		if (tryPath(chainPath("/usr/local/etc", confName), ldcDir, ret))
233 			return true;
234 		if (tryPath(chainPath("/usr/local/etc/ldc", confName), ldcDir, ret))
235 			return true;
236 	}
237 
238 	return false;
239 }
240 
241 version (Windows) private string readLdcPathFromRegistry()
242 {
243 	import std.windows.registry;
244 
245 	// https://github.com/ldc-developers/ldc/blob/829dc71114eaf7c769208f03eb9a614dafd789c3/driver/configfile.cpp#L65
246 	try
247 	{
248 		scope Key hklm = Registry.localMachine;
249 		scope Key val = hklm.getKey(`SOFTWARE\ldc-developers\LDC\0.11.0`, REGSAM.KEY_QUERY_VALUE);
250 		return val.getValue("Path").value_SZ();
251 	}
252 	catch (RegistryException)
253 	{
254 		return null;
255 	}
256 }
257 
258 private string getUserHomeDirectoryLDC()
259 {
260 	version (Windows)
261 	{
262 		import core.sys.windows.windows;
263 		import core.sys.windows.shlobj;
264 
265 		wchar[MAX_PATH] buf;
266 		HRESULT res = SHGetFolderPathW(null, CSIDL_FLAG_CREATE | CSIDL_APPDATA, null, SHGFP_TYPE
267 				.SHGFP_TYPE_CURRENT, buf.ptr);
268 		if (res != S_OK)
269 			return null;
270 
271 		auto len = buf[].countUntil(wchar('\0'));
272 		if (len == -1)
273 			len = buf.length;
274 		return buf[0 .. len].to!string;
275 	}
276 	else
277 	{
278 		string home = environment.get("HOME");
279 		return home.length ? home : "/";
280 	}
281 }
282 
283 string searchPathFor(scope const(char)[] executable)
284 {
285 	auto path = environment.get("PATH");
286 
287 	version (Posix)
288 	{
289 		enum char separator = ':';
290 		enum string exeExt = "";
291 	}
292 	else version (Windows)
293 	{
294 		enum char separator = ';';
295 		enum string exeExt = ".exe";
296 	}
297 	else
298 		static assert(false, "No path separator character");
299 
300 	static if (exeExt.length)
301 	{
302 		if (!executable.endsWith(exeExt))
303 			executable ~= exeExt;
304 	}
305 
306 	foreach (dir; path.splitter(separator))
307 	{
308 		auto execPath = buildPath(dir, executable);
309 		if (fs.exists(execPath))
310 			return execPath;
311 	}
312 
313 	return null;
314 }
315 
316 deprecated bool parseDmdConfImports(R)(R path, out string[] paths)
317 {
318 	return parseDmdConfImports(path, dirName(path).to!string, paths);
319 }
320 
321 bool parseDmdConfImports(R)(R confPath, scope const(char)[] confDirPath, out string[] paths)
322 {
323 	enum Region
324 	{
325 		none,
326 		env32,
327 		env64
328 	}
329 
330 	Region match, current;
331 
332 	trace("test dmd conf ", confPath);
333 	foreach (line; io.File(confPath).byLine)
334 	{
335 		line = line.strip;
336 		if (!line.length)
337 			continue;
338 
339 		if (line.sicmp("[Environment32]") == 0)
340 			current = Region.env32;
341 		else if (line.sicmp("[Environment64]") == 0)
342 			current = Region.env64;
343 		else if (line.startsWith("DFLAGS=") && current >= match)
344 		{
345 			version (Windows)
346 				paths = parseDflagsImports(line["DFLAGS=".length .. $].stripLeft, confDirPath, true);
347 			else
348 				paths = parseDflagsImports(line["DFLAGS=".length .. $]
349 						.stripLeft, confDirPath, false);
350 			match = current;
351 		}
352 	}
353 
354 	bool ret = match != Region.none || paths.length > 0;
355 	if (!ret)
356 		warning("failed to find phobos/druntime paths in dmd conf ", confPath, " - going to continue looking elsewhere...");
357 	return ret;
358 }
359 
360 bool parseLdcConfImports(string confPath, scope const(char)[] binDirPath, out string[] paths)
361 {
362 	import external.ldc.config;
363 
364 	auto ret = appender!(string[]);
365 
366 	binDirPath = binDirPath.replace('\\', '/');
367 
368 	void handleSwitch(string value)
369 	{
370 		if (value.startsWith("-I"))
371 			ret ~= value[2 .. $];
372 	}
373 
374 	void parseSection(GroupSetting section)
375 	{
376 		foreach (c; section.children)
377 		{
378 			if (c.type == Setting.Type.array
379 					&& (c.name == "switches" || c.name == "post-switches"))
380 			{
381 				if (auto input = cast(ArraySetting) c)
382 				{
383 					foreach (sw; input.vals)
384 						handleSwitch(sw.replace("%%ldcbinarypath%%", binDirPath));
385 				}
386 			}
387 		}
388 	}
389 
390 	trace("test ldc conf ", confPath);
391 	foreach (s; parseConfigFile(confPath))
392 	{
393 		if (s.type == Setting.Type.group && s.name == "default")
394 		{
395 			parseSection(cast(GroupSetting) s);
396 		}
397 	}
398 
399 	paths = ret.data;
400 	if (!ret.data.length)
401 		warning("failed to find phobos/druntime paths in ldc conf ", confPath, " - going to continue looking elsewhere...");
402 	return ret.data.length > 0;
403 }
404 
405 deprecated string[] parseDflagsImports(scope const(char)[] options, bool windows)
406 {
407 	return parseDflagsImports(options, null, windows);
408 }
409 
410 string[] parseDflagsImports(scope const(char)[] options, scope const(char)[] cwd, bool windows)
411 {
412 	auto ret = appender!(string[]);
413 	size_t i = options.indexOf("-I");
414 	while (i != cast(size_t)-1)
415 	{
416 		if (i == 0 || options[i - 1] == '"' || options[i - 1] == '\'' || options[i - 1] == ' ')
417 		{
418 			dchar quote = i == 0 ? ' ' : options[i - 1];
419 			i += "-I".length;
420 			ret.put(parseArgumentWord(options, quote, i, windows).replace("%@P%", cwd));
421 		}
422 		else
423 			i += "-I".length;
424 
425 		i = options.indexOf("-I", i);
426 	}
427 	return ret.data;
428 }
429 
430 private string parseArgumentWord(const scope char[] data, dchar quote, ref size_t i, bool windows)
431 {
432 	bool allowEscapes = quote != '\'';
433 	bool inEscape;
434 	bool ending = quote == ' ';
435 	auto part = appender!string;
436 	while (i < data.length)
437 	{
438 		auto c = decode!(UseReplacementDchar.yes)(data, i);
439 		if (inEscape)
440 		{
441 			part.put(c);
442 			inEscape = false;
443 		}
444 		else if (ending)
445 		{
446 			// -I"abc"def
447 			// or
448 			// -I'abc'\''def'
449 			if (c.isWhite)
450 				break;
451 			else if (c == '\\' && !windows)
452 				inEscape = true;
453 			else if (c == '\'')
454 			{
455 				quote = c;
456 				allowEscapes = false;
457 				ending = false;
458 			}
459 			else if (c == '"')
460 			{
461 				quote = c;
462 				allowEscapes = true;
463 				ending = false;
464 			}
465 			else
466 				part.put(c);
467 		}
468 		else
469 		{
470 			if (c == quote)
471 				ending = true;
472 			else if (c == '\\' && allowEscapes && !windows)
473 				inEscape = true;
474 			else
475 				part.put(c);
476 		}
477 	}
478 	return part.data;
479 }
480 
481 unittest
482 {
483 	void test(string input, string[] expect)
484 	{
485 		auto actual = parseDflagsImports(input, false);
486 		assert(actual == expect, actual.to!string ~ " != " ~ expect.to!string);
487 	}
488 
489 	test(`a`, []);
490 	test(`-I`, [``]);
491 	test(`-Iabc`, [`abc`]);
492 	test(`-Iab\\cd -Ief`, [`ab\cd`, `ef`]);
493 	test(`-Iab\ cd -Ief`, [`ab cd`, `ef`]);
494 	test(`-I/usr/include/dmd/phobos -I/usr/include/dmd/druntime/import -L-L/usr/lib/x86_64-linux-gnu -L--export-dynamic -fPIC`,
495 			[`/usr/include/dmd/phobos`, `/usr/include/dmd/druntime/import`]);
496 	test(`-I/usr/include/dmd/phobos -L-L/usr/lib/x86_64-linux-gnu -I/usr/include/dmd/druntime/import -L--export-dynamic -fPIC`,
497 			[`/usr/include/dmd/phobos`, `/usr/include/dmd/druntime/import`]);
498 }