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 }