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 : warning, trace;
8 import std.path : buildNormalizedPath, baseName, buildPath, chainPath, dirName, stripExtension, isAbsolute;
9 import std.process : environment;
10 import std.string : 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 		trace("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 
112 	version (Windows)
113 	{
114 		if (dmdPath.length)
115 		{
116 			auto dmdDir = dirName(dmdPath);
117 			bool haveDRuntime = fs.exists(chainPath(dmdDir, "..", "..", "src",
118 					"druntime", "import"));
119 			bool havePhobos = fs.exists(chainPath(dmdDir, "..", "..", "src", "phobos"));
120 			if (haveDRuntime && havePhobos)
121 				ret = [
122 					buildNormalizedPath(dmdDir, "..", "..", "src", "druntime",
123 							"import"),
124 					buildNormalizedPath(dmdDir, "..", "..", "src", "phobos")
125 				];
126 			else if (haveDRuntime)
127 				ret = [
128 					buildNormalizedPath(dmdDir, "..", "..", "src", "druntime", "import")
129 				];
130 			else if (havePhobos)
131 				ret = [buildNormalizedPath(dmdDir, "..", "..", "src", "phobos")];
132 
133 			return ret.length > 0;
134 		}
135 		else
136 		{
137 			return false;
138 		}
139 	}
140 	else version (Posix)
141 	{
142 		if (fs.exists("/etc/dmd.conf") && parseDmdConfImports("/etc/dmd.conf", "/etc", ret))
143 			return true;
144 
145 		return false;
146 	}
147 	else
148 	{
149 		pragma(msg,
150 				__FILE__ ~ "(" ~ __LINE__
151 				~ "): Note: Unknown target OS. Please add default dmd stdlib path");
152 		return false;
153 	}
154 }
155 
156 bool detectLDCStdlibPaths(string cwd, out string[] ret, string ldcPath = null)
157 {
158 	// https://github.com/ldc-developers/ldc/blob/829dc71114eaf7c769208f03eb9a614dafd789c3/driver/configfile.cpp
159 
160 	static bool tryPath(R)(R path, lazy scope const(char)[] pathDir, out string[] ret)
161 	{
162 		return fs.exists(path) && parseLdcConfImports(path.to!string, pathDir, ret);
163 	}
164 
165 	static immutable confName = "ldc2.conf";
166 	version (Windows)
167 	{
168 		static immutable ldcExe = "ldc2.exe";
169 		static immutable ldcExeAlt = "ldc.exe";
170 	}
171 	else
172 	{
173 		static immutable ldcExe = "ldc2";
174 		static immutable ldcExeAlt = "ldc";
175 	}
176 
177 	if (!ldcPath.length || !fs.exists(ldcPath))
178 		ldcPath = searchPathFor(ldcExe);
179 
180 	if (!ldcPath.length)
181 		ldcPath = searchPathFor(ldcExeAlt);
182 	auto ldcDir = ldcPath.length ? dirName(ldcPath) : null;
183 
184 	if (tryPath(chainPath(cwd, confName), ldcDir, ret))
185 		return true;
186 
187 	if (ldcPath.length)
188 	{
189 		if (tryPath(chainPath(ldcDir, confName), ldcDir, ret))
190 			return true;
191 	}
192 
193 	string home = getUserHomeDirectoryLDC();
194 	if (home.length)
195 	{
196 		if (tryPath(chainPath(home, ".ldc", confName), ldcDir, ret))
197 			return true;
198 
199 		version (Windows)
200 		{
201 			if (tryPath(chainPath(home, confName), ldcDir, ret))
202 				return true;
203 		}
204 	}
205 
206 	if (ldcPath.length)
207 	{
208 		if (tryPath(chainPath(ldcDir.dirName, "etc", confName), ldcDir, ret))
209 			return true;
210 	}
211 
212 	version (Windows)
213 	{
214 		string path = readLdcPathFromRegistry();
215 		if (path.length && tryPath(chainPath(path, "etc", confName), ldcDir, ret))
216 			return true;
217 	}
218 	else
219 	{
220 		if (tryPath(chainPath("/etc", confName), ldcDir, ret))
221 			return true;
222 		if (tryPath(chainPath("/etc/ldc", confName), ldcDir, ret))
223 			return true;
224 	}
225 
226 	return false;
227 }
228 
229 version (Windows) private string readLdcPathFromRegistry()
230 {
231 	import std.windows.registry;
232 
233 	// https://github.com/ldc-developers/ldc/blob/829dc71114eaf7c769208f03eb9a614dafd789c3/driver/configfile.cpp#L65
234 	try
235 	{
236 		scope Key hklm = Registry.localMachine;
237 		scope Key val = hklm.getKey(`SOFTWARE\ldc-developers\LDC\0.11.0`, REGSAM.KEY_QUERY_VALUE);
238 		return val.getValue("Path").value_SZ();
239 	}
240 	catch (RegistryException)
241 	{
242 		return null;
243 	}
244 }
245 
246 private string getUserHomeDirectoryLDC()
247 {
248 	version (Windows)
249 	{
250 		import core.sys.windows.windows;
251 		import core.sys.windows.shlobj;
252 
253 		wchar[MAX_PATH] buf;
254 		HRESULT res = SHGetFolderPathW(null, CSIDL_FLAG_CREATE | CSIDL_APPDATA, null, SHGFP_TYPE
255 				.SHGFP_TYPE_CURRENT, buf.ptr);
256 		if (res != S_OK)
257 			return null;
258 
259 		auto len = buf[].countUntil(wchar('\0'));
260 		if (len == -1)
261 			len = buf.length;
262 		return buf[0 .. len].to!string;
263 	}
264 	else
265 	{
266 		string home = environment.get("HOME");
267 		return home.length ? home : "/";
268 	}
269 }
270 
271 private string searchPathFor()(scope const(char)[] executable)
272 {
273 	auto path = environment.get("PATH");
274 
275 	version (Posix)
276 		char separator = ':';
277 	else version (Windows)
278 		char separator = ';';
279 	else
280 		static assert(false, "No path separator character");
281 
282 	foreach (dir; path.splitter(separator))
283 	{
284 		auto execPath = buildPath(dir, executable);
285 		if (fs.exists(execPath))
286 			return execPath;
287 	}
288 
289 	return null;
290 }
291 
292 deprecated bool parseDmdConfImports(R)(R path, out string[] paths)
293 {
294 	return parseDmdConfImports(path, dirName(path).to!string, paths);
295 }
296 
297 bool parseDmdConfImports(R)(R confPath, scope const(char)[] confDirPath, out string[] paths)
298 {
299 	enum Region
300 	{
301 		none,
302 		env32,
303 		env64
304 	}
305 
306 	Region match, current;
307 
308 	trace("test dmd conf ", confPath);
309 	foreach (line; io.File(confPath).byLine)
310 	{
311 		line = line.strip;
312 		if (!line.length)
313 			continue;
314 
315 		if (line.sicmp("[Environment32]") == 0)
316 			current = Region.env32;
317 		else if (line.sicmp("[Environment64]") == 0)
318 			current = Region.env64;
319 		else if (line.startsWith("DFLAGS=") && current >= match)
320 		{
321 			version (Windows)
322 				paths = parseDflagsImports(line["DFLAGS=".length .. $].stripLeft, confDirPath, true);
323 			else
324 				paths = parseDflagsImports(line["DFLAGS=".length .. $]
325 						.stripLeft, confDirPath, false);
326 			match = current;
327 		}
328 	}
329 
330 	return match != Region.none || paths.length > 0;
331 }
332 
333 bool parseLdcConfImports(string confPath, scope const(char)[] binDirPath, out string[] paths)
334 {
335 	import external.ldc.config;
336 
337 	auto ret = appender!(string[]);
338 
339 	binDirPath = binDirPath.replace('\\', '/');
340 
341 	void handleSwitch(string value)
342 	{
343 		if (value.startsWith("-I"))
344 			ret ~= value[2 .. $];
345 	}
346 
347 	void parseSection(GroupSetting section)
348 	{
349 		foreach (c; section.children)
350 		{
351 			if (c.type == Setting.Type.array
352 					&& (c.name == "switches" || c.name == "post-switches"))
353 			{
354 				if (auto input = cast(ArraySetting) c)
355 				{
356 					foreach (sw; input.vals)
357 						handleSwitch(sw.replace("%%ldcbinarypath%%", binDirPath));
358 				}
359 			}
360 		}
361 	}
362 
363 	trace("test ldc conf ", confPath);
364 	foreach (s; parseConfigFile(confPath))
365 	{
366 		if (s.type == Setting.Type.group && s.name == "default")
367 		{
368 			parseSection(cast(GroupSetting) s);
369 		}
370 	}
371 
372 	paths = ret.data;
373 	return ret.data.length > 0;
374 }
375 
376 deprecated string[] parseDflagsImports(scope const(char)[] options, bool windows)
377 {
378 	return parseDflagsImports(options, null, windows);
379 }
380 
381 string[] parseDflagsImports(scope const(char)[] options, scope const(char)[] cwd, bool windows)
382 {
383 	auto ret = appender!(string[]);
384 	size_t i = options.indexOf("-I");
385 	while (i != cast(size_t)-1)
386 	{
387 		if (i == 0 || options[i - 1] == '"' || options[i - 1] == '\'' || options[i - 1] == ' ')
388 		{
389 			dchar quote = i == 0 ? ' ' : options[i - 1];
390 			i += "-I".length;
391 			ret.put(parseArgumentWord(options, quote, i, windows).replace("%@P%", cwd));
392 		}
393 		else
394 			i += "-I".length;
395 
396 		i = options.indexOf("-I", i);
397 	}
398 	return ret.data;
399 }
400 
401 private string parseArgumentWord(const scope char[] data, dchar quote, ref size_t i, bool windows)
402 {
403 	bool allowEscapes = quote != '\'';
404 	bool inEscape;
405 	bool ending = quote == ' ';
406 	auto part = appender!string;
407 	while (i < data.length)
408 	{
409 		auto c = decode!(UseReplacementDchar.yes)(data, i);
410 		if (inEscape)
411 		{
412 			part.put(c);
413 			inEscape = false;
414 		}
415 		else if (ending)
416 		{
417 			// -I"abc"def
418 			// or
419 			// -I'abc'\''def'
420 			if (c.isWhite)
421 				break;
422 			else if (c == '\\' && !windows)
423 				inEscape = true;
424 			else if (c == '\'')
425 			{
426 				quote = c;
427 				allowEscapes = false;
428 				ending = false;
429 			}
430 			else if (c == '"')
431 			{
432 				quote = c;
433 				allowEscapes = true;
434 				ending = false;
435 			}
436 			else
437 				part.put(c);
438 		}
439 		else
440 		{
441 			if (c == quote)
442 				ending = true;
443 			else if (c == '\\' && allowEscapes && !windows)
444 				inEscape = true;
445 			else
446 				part.put(c);
447 		}
448 	}
449 	return part.data;
450 }
451 
452 unittest
453 {
454 	void test(string input, string[] expect)
455 	{
456 		auto actual = parseDflagsImports(input, false);
457 		assert(actual == expect, actual.to!string ~ " != " ~ expect.to!string);
458 	}
459 
460 	test(`a`, []);
461 	test(`-I`, [``]);
462 	test(`-Iabc`, [`abc`]);
463 	test(`-Iab\\cd -Ief`, [`ab\cd`, `ef`]);
464 	test(`-Iab\ cd -Ief`, [`ab cd`, `ef`]);
465 	test(`-I/usr/include/dmd/phobos -I/usr/include/dmd/druntime/import -L-L/usr/lib/x86_64-linux-gnu -L--export-dynamic -fPIC`,
466 			[`/usr/include/dmd/phobos`, `/usr/include/dmd/druntime/import`]);
467 	test(`-I/usr/include/dmd/phobos -L-L/usr/lib/x86_64-linux-gnu -I/usr/include/dmd/druntime/import -L--export-dynamic -fPIC`,
468 			[`/usr/include/dmd/phobos`, `/usr/include/dmd/druntime/import`]);
469 }