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 } 479 480 string stripUTF8Bom(string str) 481 { 482 if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) 483 return str[3 ..$]; 484 return str; 485 } 486 487 private bool isNumber(string str) { 488 foreach (ch; str) 489 switch (ch) { 490 case '0': .. case '9': break; 491 default: return false; 492 } 493 return true; 494 } 495 496 private bool isHexNumber(string str) { 497 foreach (ch; str) 498 switch (ch) { 499 case '0': .. case '9': break; 500 case 'a': .. case 'f': break; 501 case 'A': .. case 'F': break; 502 default: return false; 503 } 504 return true; 505 } 506 507 /** 508 Get the closest match of $(D input) in the $(D array), where $(D distance) 509 is the maximum levenshtein distance allowed between the compared strings. 510 Returns $(D null) if no closest match is found. 511 */ 512 string getClosestMatch(string[] array, string input, size_t distance) 513 { 514 import std.algorithm : countUntil, map, levenshteinDistance; 515 import std.uni : toUpper; 516 517 auto distMap = array.map!(elem => 518 levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input)); 519 auto idx = distMap.countUntil!(a => a <= distance); 520 return (idx == -1) ? null : array[idx]; 521 } 522 523 /** 524 Searches for close matches to input in range. R must be a range of strings 525 Note: Sorts the strings range. Use std.range.indexed to avoid this... 526 */ 527 auto fuzzySearch(R)(R strings, string input){ 528 import std.algorithm : levenshteinDistance, schwartzSort, partition3; 529 import std.traits : isSomeString; 530 import std.range : ElementType; 531 532 static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang"); 533 immutable threshold = input.length / 4; 534 return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1] 535 .schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper)); 536 } 537 538 /** 539 If T is a bitfield-style enum, this function returns a string range 540 listing the names of all members included in the given value. 541 542 Example: 543 --------- 544 enum Bits { 545 none = 0, 546 a = 1<<0, 547 b = 1<<1, 548 c = 1<<2, 549 a_c = a | c, 550 } 551 552 assert( bitFieldNames(Bits.none).equals(["none"]) ); 553 assert( bitFieldNames(Bits.a).equals(["a"]) ); 554 assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) ); 555 --------- 556 */ 557 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T) 558 { 559 import std.algorithm : filter, map; 560 import std.conv : to; 561 import std.traits : EnumMembers; 562 563 return [ EnumMembers!(T) ] 564 .filter!(member => member==0? value==0 : (value & member) == member) 565 .map!(member => to!string(member)); 566 } 567 568 569 bool isIdentChar(dchar ch) 570 { 571 import std.ascii : isAlphaNum; 572 return isAlphaNum(ch) || ch == '_'; 573 } 574 575 string stripDlangSpecialChars(string s) 576 { 577 import std.array : appender; 578 auto ret = appender!string(); 579 foreach(ch; s) 580 ret.put(isIdentChar(ch) ? ch : '_'); 581 return ret.data; 582 } 583 584 string determineModuleName(BuildSettings settings, NativePath file, NativePath base_path) 585 { 586 import std.algorithm : map; 587 import std.array : array; 588 import std.range : walkLength; 589 590 assert(base_path.absolute); 591 if (!file.absolute) file = base_path ~ file; 592 593 size_t path_skip = 0; 594 foreach (ipath; settings.importPaths.map!(p => NativePath(p))) { 595 if (!ipath.absolute) ipath = base_path ~ ipath; 596 assert(!ipath.empty); 597 if (file.startsWith(ipath) && ipath.bySegment.walkLength > path_skip) 598 path_skip = ipath.bySegment.walkLength; 599 } 600 601 enforce(path_skip > 0, 602 format("Source file '%s' not found in any import path.", file.toNativeString())); 603 604 auto mpath = file.bySegment.array[path_skip .. $]; 605 auto ret = appender!string; 606 607 //search for module keyword in file 608 string moduleName = getModuleNameFromFile(file.to!string); 609 610 if(moduleName.length) return moduleName; 611 612 //create module name from path 613 foreach (i; 0 .. mpath.length) { 614 import std.path; 615 auto p = mpath[i].name; 616 if (p == "package.d") break; 617 if (i > 0) ret ~= "."; 618 if (i+1 < mpath.length) ret ~= p; 619 else ret ~= p.baseName(".d"); 620 } 621 622 return ret.data; 623 } 624 625 /** 626 * Search for module keyword in D Code 627 */ 628 string getModuleNameFromContent(string content) { 629 import std.regex; 630 import std.string; 631 632 content = content.strip; 633 if (!content.length) return null; 634 635 static bool regex_initialized = false; 636 static Regex!char comments_pattern, module_pattern; 637 638 if (!regex_initialized) { 639 comments_pattern = regex(`//[^\r\n]*\r?\n?|/\*.*?\*/|/\+.*\+/`, "gs"); 640 module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g"); 641 regex_initialized = true; 642 } 643 644 content = replaceAll(content, comments_pattern, " "); 645 auto result = matchFirst(content, module_pattern); 646 647 if (!result.empty) return result[1]; 648 649 return null; 650 } 651 652 unittest { 653 assert(getModuleNameFromContent("") == ""); 654 assert(getModuleNameFromContent("module myPackage.myModule;") == "myPackage.myModule"); 655 assert(getModuleNameFromContent("module \t\n myPackage.myModule \t\r\n;") == "myPackage.myModule"); 656 assert(getModuleNameFromContent("// foo\nmodule bar;") == "bar"); 657 assert(getModuleNameFromContent("/*\nfoo\n*/\nmodule bar;") == "bar"); 658 assert(getModuleNameFromContent("/+\nfoo\n+/\nmodule bar;") == "bar"); 659 assert(getModuleNameFromContent("/***\nfoo\n***/\nmodule bar;") == "bar"); 660 assert(getModuleNameFromContent("/+++\nfoo\n+++/\nmodule bar;") == "bar"); 661 assert(getModuleNameFromContent("// module foo;\nmodule bar;") == "bar"); 662 assert(getModuleNameFromContent("/* module foo; */\nmodule bar;") == "bar"); 663 assert(getModuleNameFromContent("/+ module foo; +/\nmodule bar;") == "bar"); 664 assert(getModuleNameFromContent("/+ /+ module foo; +/ +/\nmodule bar;") == "bar"); 665 assert(getModuleNameFromContent("// module foo;\nmodule bar; // module foo;") == "bar"); 666 assert(getModuleNameFromContent("// module foo;\nmodule// module foo;\nbar//module foo;\n;// module foo;") == "bar"); 667 assert(getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;") == "bar", getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;")); 668 assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar;") == "bar"); 669 //assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar/++/;") == "bar"); // nested comments require a context-free parser! 670 assert(getModuleNameFromContent("/*\nmodule sometest;\n*/\n\nmodule fakemath;\n") == "fakemath"); 671 } 672 673 /** 674 * Search for module keyword in file 675 */ 676 string getModuleNameFromFile(string filePath) { 677 string fileContent = filePath.readText; 678 679 logDiagnostic("Get module name from path: " ~ filePath); 680 return getModuleNameFromContent(fileContent); 681 }