1 /** 2 ... 3 4 Copyright: © 2012 Matthias Dondorff 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Matthias Dondorff 7 */ 8 module dub.internal.utils; 9 10 import dub.internal.vibecompat.core.file; 11 import dub.internal.vibecompat.core.log; 12 import dub.internal.vibecompat.data.json; 13 import dub.internal.vibecompat.inet.url; 14 import dub.compilers.buildsettings : BuildSettings; 15 import dub.version_; 16 17 import core.time : Duration; 18 import std.algorithm : canFind, startsWith; 19 import std.array : appender, array; 20 import std.conv : to; 21 import std.exception : enforce; 22 import std.file; 23 import std..string : format; 24 import std.process; 25 import std.traits : isIntegral; 26 version(DubUseCurl) 27 { 28 import std.net.curl; 29 public import std.net.curl : HTTPStatusException; 30 } 31 32 33 private NativePath[] temporary_files; 34 35 NativePath getTempDir() 36 { 37 return NativePath(std.file.tempDir()); 38 } 39 40 NativePath getTempFile(string prefix, string extension = null) 41 { 42 import std.uuid : randomUUID; 43 import std.array: replace; 44 45 string fileName = prefix ~ "-" ~ randomUUID.toString() ~ extension; 46 47 if (extension !is null && extension == ".d") 48 fileName = fileName.replace("-", "_"); 49 50 auto path = getTempDir() ~ fileName; 51 temporary_files ~= path; 52 return path; 53 } 54 55 /** 56 Obtain a lock for a file at the given path. If the file cannot be locked 57 within the given duration, an exception is thrown. The file will be created 58 if it does not yet exist. Deleting the file is not safe as another process 59 could create a new file with the same name. 60 The returned lock will get unlocked upon destruction. 61 62 Params: 63 path = path to file that gets locked 64 timeout = duration after which locking failed 65 Returns: 66 The locked file or an Exception on timeout. 67 */ 68 auto lockFile(string path, Duration timeout) 69 { 70 import core.thread : Thread; 71 import std.datetime, std.stdio : File; 72 import std.algorithm : move; 73 74 // Just a wrapper to hide (and destruct) the locked File. 75 static struct LockFile 76 { 77 // The Lock can't be unlinked as someone could try to lock an already 78 // opened fd while a new file with the same name gets created. 79 // Exclusive filesystem locks (O_EXCL, mkdir) could be deleted but 80 // aren't automatically freed when a process terminates, see #1149. 81 private File f; 82 } 83 84 auto file = File(path, "w"); 85 auto t0 = Clock.currTime(); 86 auto dur = 1.msecs; 87 while (true) 88 { 89 if (file.tryLock()) 90 return LockFile(move(file)); 91 enforce(Clock.currTime() - t0 < timeout, "Failed to lock '"~path~"'."); 92 if (dur < 1024.msecs) // exponentially increase sleep time 93 dur *= 2; 94 Thread.sleep(dur); 95 } 96 } 97 98 static ~this() 99 { 100 foreach (path; temporary_files) 101 { 102 auto spath = path.toNativeString(); 103 if (spath.exists) 104 std.file.remove(spath); 105 } 106 } 107 108 bool isWritableDir(NativePath p, bool create_if_missing = false) 109 { 110 import std.random; 111 auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max)); 112 if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString()); 113 try openFile(fname, FileMode.createTrunc).close(); 114 catch (Exception) return false; 115 remove(fname.toNativeString()); 116 return true; 117 } 118 119 Json jsonFromFile(NativePath file, bool silent_fail = false) { 120 if( silent_fail && !existsFile(file) ) return Json.emptyObject; 121 auto f = openFile(file.toNativeString(), FileMode.read); 122 scope(exit) f.close(); 123 auto text = stripUTF8Bom(cast(string)f.readAll()); 124 return parseJsonString(text, file.toNativeString()); 125 } 126 127 /** 128 Read package info file content from archive. 129 File needs to be in root folder or in first 130 sub folder. 131 132 Params: 133 zip = path to archive file 134 fileName = Package file name 135 Returns: 136 package file content. 137 */ 138 string packageInfoFileFromZip(NativePath zip, out string fileName) { 139 import std.zip : ZipArchive, ArchiveMember; 140 import dub.package_ : packageInfoFiles; 141 142 auto f = openFile(zip, FileMode.read); 143 ubyte[] b = new ubyte[cast(size_t)f.size]; 144 f.rawRead(b); 145 f.close(); 146 auto archive = new ZipArchive(b); 147 alias PSegment = typeof (NativePath.init.head); 148 foreach (ArchiveMember am; archive.directory) { 149 auto path = NativePath(am.name).bySegment.array; 150 foreach (fil; packageInfoFiles) { 151 if ((path.length == 1 && path[0] == fil.filename) || (path.length == 2 && path[$-1].name == fil.filename)) { 152 fileName = fil.filename; 153 return stripUTF8Bom(cast(string) archive.expand(archive.directory[am.name])); 154 } 155 } 156 } 157 throw new Exception("No package descriptor found"); 158 } 159 160 void writeJsonFile(NativePath path, Json json) 161 { 162 auto f = openFile(path, FileMode.createTrunc); 163 scope(exit) f.close(); 164 f.writePrettyJsonString(json); 165 } 166 167 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file 168 void atomicWriteJsonFile(NativePath path, Json json) 169 { 170 import std.random : uniform; 171 auto tmppath = path.parentPath ~ format("%s.%s.tmp", path.head, uniform(0, int.max)); 172 auto f = openFile(tmppath, FileMode.createTrunc); 173 scope (failure) { 174 f.close(); 175 removeFile(tmppath); 176 } 177 f.writePrettyJsonString(json); 178 f.close(); 179 if (existsFile(path)) removeFile(path); 180 moveFile(tmppath, path); 181 } 182 183 bool existsDirectory(NativePath path) { 184 if( !existsFile(path) ) return false; 185 auto fi = getFileInfo(path); 186 return fi.isDirectory; 187 } 188 189 void runCommand(string command, string[string] env = null, string workDir = null) 190 { 191 runCommands((&command)[0 .. 1], env, workDir); 192 } 193 194 void runCommands(in string[] commands, string[string] env = null, string workDir = null) 195 { 196 import std.stdio : stdin, stdout, stderr, File; 197 198 version(Windows) enum nullFile = "NUL"; 199 else version(Posix) enum nullFile = "/dev/null"; 200 else static assert(0); 201 202 auto childStdout = stdout; 203 auto childStderr = stderr; 204 auto config = Config.retainStdout | Config.retainStderr; 205 206 // Disable child's stdout/stderr depending on LogLevel 207 auto logLevel = getLogLevel(); 208 if(logLevel >= LogLevel.warn) 209 childStdout = File(nullFile, "w"); 210 if(logLevel >= LogLevel.none) 211 childStderr = File(nullFile, "w"); 212 213 foreach(cmd; commands){ 214 logDiagnostic("Running %s", cmd); 215 Pid pid; 216 pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config, workDir); 217 auto exitcode = pid.wait(); 218 enforce(exitcode == 0, "Command failed with exit code " 219 ~ to!string(exitcode) ~ ": " ~ cmd); 220 } 221 } 222 223 version (Have_vibe_d_http) 224 public import vibe.http.common : HTTPStatusException; 225 226 /** 227 Downloads a file from the specified URL. 228 229 Any redirects will be followed until the actual file resource is reached or if the redirection 230 limit of 10 is reached. Note that only HTTP(S) is currently supported. 231 232 The download times out if a connection cannot be established within 233 `timeout` ms, or if the average transfer rate drops below 10 bytes / s for 234 more than `timeout` seconds. Pass `0` as `timeout` to disable both timeout 235 mechanisms. 236 237 Note: Timeouts are only implemented when curl is used (DubUseCurl). 238 */ 239 void download(string url, string filename, uint timeout = 8) 240 { 241 version(DubUseCurl) { 242 auto conn = HTTP(); 243 setupHTTPClient(conn, timeout); 244 logDebug("Storing %s...", url); 245 std.net.curl.download(url, filename, conn); 246 // workaround https://issues.dlang.org/show_bug.cgi?id=18318 247 auto sl = conn.statusLine; 248 logDebug("Download %s %s", url, sl); 249 if (sl.code / 100 != 2) 250 throw new HTTPStatusException(sl.code, 251 "Downloading %s failed with %d (%s).".format(url, sl.code, sl.reason)); 252 } else version (Have_vibe_d_http) { 253 import vibe.inet.urltransfer; 254 vibe.inet.urltransfer.download(url, filename); 255 } else assert(false); 256 } 257 /// ditto 258 void download(URL url, NativePath filename, uint timeout = 8) 259 { 260 download(url.toString(), filename.toNativeString(), timeout); 261 } 262 /// ditto 263 ubyte[] download(string url, uint timeout = 8) 264 { 265 version(DubUseCurl) { 266 auto conn = HTTP(); 267 setupHTTPClient(conn, timeout); 268 logDebug("Getting %s...", url); 269 return cast(ubyte[])get(url, conn); 270 } else version (Have_vibe_d_http) { 271 import vibe.inet.urltransfer; 272 import vibe.stream.operations; 273 ubyte[] ret; 274 vibe.inet.urltransfer.download(url, (scope input) { ret = input.readAll(); }); 275 return ret; 276 } else assert(false); 277 } 278 /// ditto 279 ubyte[] download(URL url, uint timeout = 8) 280 { 281 return download(url.toString(), timeout); 282 } 283 284 /** 285 Downloads a file from the specified URL with retry logic. 286 287 Downloads a file from the specified URL with up to n tries on failure 288 Throws: `Exception` if the download failed or `HTTPStatusException` after the nth retry or 289 on "unrecoverable failures" such as 404 not found 290 Otherwise might throw anything else that `download` throws. 291 See_Also: download 292 293 The download times out if a connection cannot be established within 294 `timeout` ms, or if the average transfer rate drops below 10 bytes / s for 295 more than `timeout` seconds. Pass `0` as `timeout` to disable both timeout 296 mechanisms. 297 298 Note: Timeouts are only implemented when curl is used (DubUseCurl). 299 **/ 300 void retryDownload(URL url, NativePath filename, size_t retryCount = 3, uint timeout = 8) 301 { 302 foreach(i; 0..retryCount) { 303 version(DubUseCurl) { 304 try { 305 download(url, filename, timeout); 306 return; 307 } 308 catch(HTTPStatusException e) { 309 if (e.status == 404) throw e; 310 else { 311 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 312 if (i == retryCount - 1) throw e; 313 else continue; 314 } 315 } 316 catch(CurlException e) { 317 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 318 continue; 319 } 320 } 321 else 322 { 323 try { 324 download(url, filename); 325 return; 326 } 327 catch(HTTPStatusException e) { 328 if (e.status == 404) throw e; 329 else { 330 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 331 if (i == retryCount - 1) throw e; 332 else continue; 333 } 334 } 335 } 336 } 337 throw new Exception("Failed to download %s".format(url)); 338 } 339 340 ///ditto 341 ubyte[] retryDownload(URL url, size_t retryCount = 3, uint timeout = 8) 342 { 343 foreach(i; 0..retryCount) { 344 version(DubUseCurl) { 345 try { 346 return download(url, timeout); 347 } 348 catch(HTTPStatusException e) { 349 if (e.status == 404) throw e; 350 else { 351 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 352 if (i == retryCount - 1) throw e; 353 else continue; 354 } 355 } 356 catch(CurlException e) { 357 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 358 continue; 359 } 360 } 361 else 362 { 363 try { 364 return download(url); 365 } 366 catch(HTTPStatusException e) { 367 if (e.status == 404) throw e; 368 else { 369 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 370 if (i == retryCount - 1) throw e; 371 else continue; 372 } 373 } 374 } 375 } 376 throw new Exception("Failed to download %s".format(url)); 377 } 378 379 /// Returns the current DUB version in semantic version format 380 string getDUBVersion() 381 { 382 import dub.version_; 383 import std.array : split, join; 384 // convert version string to valid SemVer format 385 auto verstr = dubVersion; 386 if (verstr.startsWith("v")) verstr = verstr[1 .. $]; 387 auto parts = verstr.split("-"); 388 if (parts.length >= 3) { 389 // detect GIT commit suffix 390 if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber()) 391 verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-"); 392 } 393 return verstr; 394 } 395 396 397 /** 398 Get current executable's path if running as DUB executable, 399 or find a DUB executable if DUB is used as a library. 400 For the latter, the following locations are checked in order: 401 $(UL 402 $(LI current working directory) 403 $(LI same directory as `compilerBinary` (if supplied)) 404 $(LI all components of the `$PATH` variable) 405 ) 406 Params: 407 compilerBinary = optional path to a D compiler executable, used to locate DUB executable 408 Returns: 409 The path to a valid DUB executable 410 Throws: 411 an Exception if no valid DUB executable is found 412 */ 413 public string getDUBExePath(in string compilerBinary=null) 414 { 415 version(DubApplication) { 416 import std.file : thisExePath; 417 return thisExePath(); 418 } 419 else { 420 // this must be dub as a library 421 import std.algorithm : filter, map, splitter; 422 import std.array : array; 423 import std.file : exists, getcwd; 424 import std.path : chainPath, dirName; 425 import std.range : chain, only, take; 426 import std.process : environment; 427 428 version(Windows) { 429 enum exeName = "dub.exe"; 430 enum pathSep = ';'; 431 } 432 else { 433 enum exeName = "dub"; 434 enum pathSep = ':'; 435 } 436 437 auto dubLocs = only( 438 getcwd().chainPath(exeName), 439 compilerBinary.dirName.chainPath(exeName), 440 ) 441 .take(compilerBinary.length ? 2 : 1) 442 .chain( 443 environment.get("PATH", "") 444 .splitter(pathSep) 445 .map!(p => p.chainPath(exeName)) 446 ) 447 .filter!exists; 448 449 enforce(!dubLocs.empty, "Could not find DUB executable"); 450 return dubLocs.front.array; 451 } 452 } 453 454 455 version(DubUseCurl) { 456 void setupHTTPClient(ref HTTP conn, uint timeout) 457 { 458 static if( is(typeof(&conn.verifyPeer)) ) 459 conn.verifyPeer = false; 460 461 auto proxy = environment.get("http_proxy", null); 462 if (proxy.length) conn.proxy = proxy; 463 464 auto noProxy = environment.get("no_proxy", null); 465 if (noProxy.length) conn.handle.set(CurlOption.noproxy, noProxy); 466 467 conn.handle.set(CurlOption.encoding, ""); 468 if (timeout) { 469 // connection (TLS+TCP) times out after 8s 470 conn.handle.set(CurlOption.connecttimeout, timeout); 471 // transfers time out after 8s below 10 byte/s 472 conn.handle.set(CurlOption.low_speed_limit, 10); 473 conn.handle.set(CurlOption.low_speed_time, timeout); 474 } 475 476 conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)"); 477 478 enum CURL_NETRC_OPTIONAL = 1; 479 conn.handle.set(CurlOption.netrc, CURL_NETRC_OPTIONAL); 480 } 481 } 482 483 string stripUTF8Bom(string str) 484 { 485 if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) 486 return str[3 ..$]; 487 return str; 488 } 489 490 private bool isNumber(string str) { 491 foreach (ch; str) 492 switch (ch) { 493 case '0': .. case '9': break; 494 default: return false; 495 } 496 return true; 497 } 498 499 private bool isHexNumber(string str) { 500 foreach (ch; str) 501 switch (ch) { 502 case '0': .. case '9': break; 503 case 'a': .. case 'f': break; 504 case 'A': .. case 'F': break; 505 default: return false; 506 } 507 return true; 508 } 509 510 /** 511 Get the closest match of $(D input) in the $(D array), where $(D distance) 512 is the maximum levenshtein distance allowed between the compared strings. 513 Returns $(D null) if no closest match is found. 514 */ 515 string getClosestMatch(string[] array, string input, size_t distance) 516 { 517 import std.algorithm : countUntil, map, levenshteinDistance; 518 import std.uni : toUpper; 519 520 auto distMap = array.map!(elem => 521 levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input)); 522 auto idx = distMap.countUntil!(a => a <= distance); 523 return (idx == -1) ? null : array[idx]; 524 } 525 526 /** 527 Searches for close matches to input in range. R must be a range of strings 528 Note: Sorts the strings range. Use std.range.indexed to avoid this... 529 */ 530 auto fuzzySearch(R)(R strings, string input){ 531 import std.algorithm : levenshteinDistance, schwartzSort, partition3; 532 import std.traits : isSomeString; 533 import std.range : ElementType; 534 535 static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang"); 536 immutable threshold = input.length / 4; 537 return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1] 538 .schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper)); 539 } 540 541 /** 542 If T is a bitfield-style enum, this function returns a string range 543 listing the names of all members included in the given value. 544 545 Example: 546 --------- 547 enum Bits { 548 none = 0, 549 a = 1<<0, 550 b = 1<<1, 551 c = 1<<2, 552 a_c = a | c, 553 } 554 555 assert( bitFieldNames(Bits.none).equals(["none"]) ); 556 assert( bitFieldNames(Bits.a).equals(["a"]) ); 557 assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) ); 558 --------- 559 */ 560 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T) 561 { 562 import std.algorithm : filter, map; 563 import std.conv : to; 564 import std.traits : EnumMembers; 565 566 return [ EnumMembers!(T) ] 567 .filter!(member => member==0? value==0 : (value & member) == member) 568 .map!(member => to!string(member)); 569 } 570 571 572 bool isIdentChar(dchar ch) 573 { 574 import std.ascii : isAlphaNum; 575 return isAlphaNum(ch) || ch == '_'; 576 } 577 578 string stripDlangSpecialChars(string s) 579 { 580 import std.array : appender; 581 auto ret = appender!string(); 582 foreach(ch; s) 583 ret.put(isIdentChar(ch) ? ch : '_'); 584 return ret.data; 585 } 586 587 string determineModuleName(BuildSettings settings, NativePath file, NativePath base_path) 588 { 589 import std.algorithm : map; 590 import std.array : array; 591 import std.range : walkLength; 592 593 assert(base_path.absolute); 594 if (!file.absolute) file = base_path ~ file; 595 596 size_t path_skip = 0; 597 foreach (ipath; settings.importPaths.map!(p => NativePath(p))) { 598 if (!ipath.absolute) ipath = base_path ~ ipath; 599 assert(!ipath.empty); 600 if (file.startsWith(ipath) && ipath.bySegment.walkLength > path_skip) 601 path_skip = ipath.bySegment.walkLength; 602 } 603 604 enforce(path_skip > 0, 605 format("Source file '%s' not found in any import path.", file.toNativeString())); 606 607 auto mpath = file.bySegment.array[path_skip .. $]; 608 auto ret = appender!string; 609 610 //search for module keyword in file 611 string moduleName = getModuleNameFromFile(file.to!string); 612 613 if(moduleName.length) return moduleName; 614 615 //create module name from path 616 foreach (i; 0 .. mpath.length) { 617 import std.path; 618 auto p = mpath[i].name; 619 if (p == "package.d") break; 620 if (i > 0) ret ~= "."; 621 if (i+1 < mpath.length) ret ~= p; 622 else ret ~= p.baseName(".d"); 623 } 624 625 return ret.data; 626 } 627 628 /** 629 * Search for module keyword in D Code 630 */ 631 string getModuleNameFromContent(string content) { 632 import std.regex; 633 import std..string; 634 635 content = content.strip; 636 if (!content.length) return null; 637 638 static bool regex_initialized = false; 639 static Regex!char comments_pattern, module_pattern; 640 641 if (!regex_initialized) { 642 comments_pattern = regex(`//[^\r\n]*\r?\n?|/\*.*?\*/|/\+.*\+/`, "gs"); 643 module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g"); 644 regex_initialized = true; 645 } 646 647 content = replaceAll(content, comments_pattern, " "); 648 auto result = matchFirst(content, module_pattern); 649 650 if (!result.empty) return result[1]; 651 652 return null; 653 } 654 655 unittest { 656 assert(getModuleNameFromContent("") == ""); 657 assert(getModuleNameFromContent("module myPackage.myModule;") == "myPackage.myModule"); 658 assert(getModuleNameFromContent("module \t\n myPackage.myModule \t\r\n;") == "myPackage.myModule"); 659 assert(getModuleNameFromContent("// foo\nmodule bar;") == "bar"); 660 assert(getModuleNameFromContent("/*\nfoo\n*/\nmodule bar;") == "bar"); 661 assert(getModuleNameFromContent("/+\nfoo\n+/\nmodule bar;") == "bar"); 662 assert(getModuleNameFromContent("/***\nfoo\n***/\nmodule bar;") == "bar"); 663 assert(getModuleNameFromContent("/+++\nfoo\n+++/\nmodule bar;") == "bar"); 664 assert(getModuleNameFromContent("// module foo;\nmodule bar;") == "bar"); 665 assert(getModuleNameFromContent("/* module foo; */\nmodule bar;") == "bar"); 666 assert(getModuleNameFromContent("/+ module foo; +/\nmodule bar;") == "bar"); 667 assert(getModuleNameFromContent("/+ /+ module foo; +/ +/\nmodule bar;") == "bar"); 668 assert(getModuleNameFromContent("// module foo;\nmodule bar; // module foo;") == "bar"); 669 assert(getModuleNameFromContent("// module foo;\nmodule// module foo;\nbar//module foo;\n;// module foo;") == "bar"); 670 assert(getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;") == "bar", getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;")); 671 assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar;") == "bar"); 672 //assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar/++/;") == "bar"); // nested comments require a context-free parser! 673 assert(getModuleNameFromContent("/*\nmodule sometest;\n*/\n\nmodule fakemath;\n") == "fakemath"); 674 } 675 676 /** 677 * Search for module keyword in file 678 */ 679 string getModuleNameFromFile(string filePath) { 680 string fileContent = filePath.readText; 681 682 logDiagnostic("Get module name from path: " ~ filePath); 683 return getModuleNameFromContent(fileContent); 684 }