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 }