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