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.data.json; 12 import dub.internal.vibecompat.inet.url; 13 import dub.compilers.buildsettings : BuildSettings; 14 import dub.version_; 15 import dub.internal.logging; 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.format; 24 import std.range; 25 import std.string : format; 26 import std.process; 27 import std.traits : isIntegral; 28 version(DubUseCurl) 29 { 30 import std.net.curl; 31 public import std.net.curl : HTTPStatusException; 32 } 33 34 public import dub.internal.temp_files; 35 36 /** 37 * Obtain a lock for a file at the given path. 38 * 39 * If the file cannot be locked within the given duration, 40 * an exception is thrown. The file will be created if it does not yet exist. 41 * Deleting the file is not safe as another process could create a new file 42 * with the same name. 43 * The returned lock will get unlocked upon destruction. 44 * 45 * Params: 46 * path = path to file that gets locked 47 * timeout = duration after which locking failed 48 * 49 * Returns: 50 * The locked file or an Exception on timeout. 51 */ 52 auto lockFile(string path, Duration timeout) 53 { 54 import core.thread : Thread; 55 import std.datetime, std.stdio : File; 56 import std.algorithm : move; 57 58 // Just a wrapper to hide (and destruct) the locked File. 59 static struct LockFile 60 { 61 // The Lock can't be unlinked as someone could try to lock an already 62 // opened fd while a new file with the same name gets created. 63 // Exclusive file system locks (O_EXCL, mkdir) could be deleted but 64 // aren't automatically freed when a process terminates, see #1149. 65 private File f; 66 } 67 68 auto file = File(path, "w"); 69 auto t0 = Clock.currTime(); 70 auto dur = 1.msecs; 71 while (true) 72 { 73 if (file.tryLock()) 74 return LockFile(move(file)); 75 enforce(Clock.currTime() - t0 < timeout, "Failed to lock '"~path~"'."); 76 if (dur < 1024.msecs) // exponentially increase sleep time 77 dur *= 2; 78 Thread.sleep(dur); 79 } 80 } 81 82 bool isWritableDir(NativePath p, bool create_if_missing = false) 83 { 84 import std.random; 85 auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max)); 86 if (create_if_missing) 87 ensureDirectory(p); 88 try writeFile(fname, "Canary"); 89 catch (Exception) return false; 90 remove(fname.toNativeString()); 91 return true; 92 } 93 94 Json jsonFromFile(NativePath file, bool silent_fail = false) { 95 if( silent_fail && !existsFile(file) ) return Json.emptyObject; 96 auto text = readText(file); 97 return parseJsonString(text, file.toNativeString()); 98 } 99 100 /** 101 Read package info file content from archive. 102 File needs to be in root folder or in first 103 sub folder. 104 105 Params: 106 zip = path to archive file 107 fileName = Package file name 108 Returns: 109 package file content. 110 */ 111 string packageInfoFileFromZip(NativePath zip, out string fileName) { 112 import std.zip : ZipArchive, ArchiveMember; 113 import dub.package_ : packageInfoFiles; 114 115 auto b = readFile(zip); 116 auto archive = new ZipArchive(b); 117 alias PSegment = typeof (NativePath.init.head); 118 foreach (ArchiveMember am; archive.directory) { 119 auto path = NativePath(am.name).bySegment.array; 120 foreach (fil; packageInfoFiles) { 121 if ((path.length == 1 && path[0] == fil.filename) || (path.length == 2 && path[$-1].name == fil.filename)) { 122 fileName = fil.filename; 123 return stripUTF8Bom(cast(string) archive.expand(archive.directory[am.name])); 124 } 125 } 126 } 127 throw new Exception("No package descriptor found"); 128 } 129 130 void writeJsonFile(NativePath path, Json json) 131 { 132 auto app = appender!string(); 133 app.writePrettyJsonString(json); 134 writeFile(path, app.data); 135 } 136 137 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file 138 void atomicWriteJsonFile(NativePath path, Json json) 139 { 140 import std.random : uniform; 141 auto tmppath = path.parentPath ~ format("%s.%s.tmp", path.head, uniform(0, int.max)); 142 auto app = appender!string(); 143 app.writePrettyJsonString(json); 144 writeFile(tmppath, app.data); 145 if (existsFile(path)) removeFile(path); 146 moveFile(tmppath, path); 147 } 148 149 void runCommand(string command, string[string] env, string workDir) 150 { 151 runCommands((&command)[0 .. 1], env, workDir); 152 } 153 154 void runCommands(in string[] commands, string[string] env, string workDir) 155 { 156 import std.stdio : stdin, stdout, stderr, File; 157 158 version(Windows) enum nullFile = "NUL"; 159 else version(Posix) enum nullFile = "/dev/null"; 160 else static assert(0); 161 162 auto childStdout = stdout; 163 auto childStderr = stderr; 164 auto config = Config.retainStdout | Config.retainStderr; 165 166 // Disable child's stdout/stderr depending on LogLevel 167 auto logLevel = getLogLevel(); 168 if(logLevel >= LogLevel.warn) 169 childStdout = File(nullFile, "w"); 170 if(logLevel >= LogLevel.none) 171 childStderr = File(nullFile, "w"); 172 173 foreach(cmd; commands){ 174 logDiagnostic("Running %s", cmd); 175 Pid pid; 176 pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config, workDir); 177 auto exitcode = pid.wait(); 178 enforce(exitcode == 0, "Command failed with exit code " 179 ~ to!string(exitcode) ~ ": " ~ cmd); 180 } 181 } 182 183 version (Have_vibe_d_http) 184 public import vibe.http.common : HTTPStatusException; 185 186 /** 187 Downloads a file from the specified URL. 188 189 Any redirects will be followed until the actual file resource is reached or if the redirection 190 limit of 10 is reached. Note that only HTTP(S) is currently supported. 191 192 The download times out if a connection cannot be established within 193 `timeout` ms, or if the average transfer rate drops below 10 bytes / s for 194 more than `timeout` seconds. Pass `0` as `timeout` to disable both timeout 195 mechanisms. 196 197 Note: Timeouts are only implemented when curl is used (DubUseCurl). 198 */ 199 private void download(string url, string filename, uint timeout = 8) 200 { 201 version(DubUseCurl) { 202 auto conn = HTTP(); 203 setupHTTPClient(conn, timeout); 204 logDebug("Storing %s...", url); 205 std.net.curl.download(url, filename, conn); 206 // workaround https://issues.dlang.org/show_bug.cgi?id=18318 207 auto sl = conn.statusLine; 208 logDebug("Download %s %s", url, sl); 209 if (sl.code / 100 != 2) 210 throw new HTTPStatusException(sl.code, 211 "Downloading %s failed with %d (%s).".format(url, sl.code, sl.reason)); 212 } else version (Have_vibe_d_http) { 213 import vibe.inet.urltransfer; 214 vibe.inet.urltransfer.download(url, filename); 215 } else assert(false); 216 } 217 /// ditto 218 private void download(URL url, NativePath filename, uint timeout = 8) 219 { 220 download(url.toString(), filename.toNativeString(), timeout); 221 } 222 /// ditto 223 private ubyte[] download(string url, uint timeout = 8) 224 { 225 version(DubUseCurl) { 226 auto conn = HTTP(); 227 setupHTTPClient(conn, timeout); 228 logDebug("Getting %s...", url); 229 return get!(HTTP, ubyte)(url, conn); 230 } else version (Have_vibe_d_http) { 231 import vibe.inet.urltransfer; 232 import vibe.stream.operations; 233 ubyte[] ret; 234 vibe.inet.urltransfer.download(url, (scope input) { ret = input.readAll(); }); 235 return ret; 236 } else assert(false); 237 } 238 /// ditto 239 private ubyte[] download(URL url, uint timeout = 8) 240 { 241 return download(url.toString(), timeout); 242 } 243 244 /** 245 Downloads a file from the specified URL with retry logic. 246 247 Downloads a file from the specified URL with up to n tries on failure 248 Throws: `Exception` if the download failed or `HTTPStatusException` after the nth retry or 249 on "unrecoverable failures" such as 404 not found 250 Otherwise might throw anything else that `download` throws. 251 See_Also: download 252 253 The download times out if a connection cannot be established within 254 `timeout` ms, or if the average transfer rate drops below 10 bytes / s for 255 more than `timeout` seconds. Pass `0` as `timeout` to disable both timeout 256 mechanisms. 257 258 Note: Timeouts are only implemented when curl is used (DubUseCurl). 259 **/ 260 void retryDownload(URL url, NativePath filename, size_t retryCount = 3, uint timeout = 8) 261 { 262 foreach(i; 0..retryCount) { 263 version(DubUseCurl) { 264 try { 265 download(url, filename, timeout); 266 return; 267 } 268 catch(HTTPStatusException e) { 269 if (e.status == 404) throw e; 270 else { 271 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 272 if (i == retryCount - 1) throw e; 273 else continue; 274 } 275 } 276 catch(CurlException e) { 277 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 278 continue; 279 } 280 } 281 else 282 { 283 try { 284 download(url, filename); 285 return; 286 } 287 catch(HTTPStatusException e) { 288 if (e.status == 404) throw e; 289 else { 290 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 291 if (i == retryCount - 1) throw e; 292 else continue; 293 } 294 } 295 } 296 } 297 throw new Exception("Failed to download %s".format(url)); 298 } 299 300 ///ditto 301 ubyte[] retryDownload(URL url, size_t retryCount = 3, uint timeout = 8) 302 { 303 foreach(i; 0..retryCount) { 304 version(DubUseCurl) { 305 try { 306 return download(url, timeout); 307 } 308 catch(HTTPStatusException e) { 309 if (e.status == 404) throw e; 310 else { 311 logDebug("Failed to download %s (Attempt %s of %s): %s", 312 url, i + 1, retryCount, e.message); 313 if (i == retryCount - 1) throw e; 314 else continue; 315 } 316 } 317 catch(CurlException e) { 318 logDebug("Failed to download %s (Attempt %s of %s): %s", 319 url, i + 1, retryCount, e.message); 320 continue; 321 } 322 } 323 else 324 { 325 try { 326 return download(url); 327 } 328 catch(HTTPStatusException e) { 329 if (e.status == 404) throw e; 330 else { 331 logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount); 332 if (i == retryCount - 1) throw e; 333 else continue; 334 } 335 } 336 } 337 } 338 throw new Exception("Failed to download %s".format(url)); 339 } 340 341 /// Returns the current DUB version in semantic version format 342 string getDUBVersion() 343 { 344 import dub.version_; 345 import std.array : split, join; 346 // convert version string to valid SemVer format 347 auto verstr = dubVersion; 348 if (verstr.startsWith("v")) verstr = verstr[1 .. $]; 349 auto parts = verstr.split("-"); 350 if (parts.length >= 3) { 351 // detect GIT commit suffix 352 if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber()) 353 verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-"); 354 } 355 return verstr; 356 } 357 358 359 /** 360 Get current executable's path if running as DUB executable, 361 or find a DUB executable if DUB is used as a library. 362 For the latter, the following locations are checked in order: 363 $(UL 364 $(LI current working directory) 365 $(LI same directory as `compilerBinary` (if supplied)) 366 $(LI all components of the `$PATH` variable) 367 ) 368 Params: 369 compilerBinary = optional path to a D compiler executable, used to locate DUB executable 370 Returns: 371 The path to a valid DUB executable 372 Throws: 373 an Exception if no valid DUB executable is found 374 */ 375 public NativePath getDUBExePath(in string compilerBinary=null) 376 { 377 version(DubApplication) { 378 import std.file : thisExePath; 379 return NativePath(thisExePath()); 380 } 381 else { 382 // this must be dub as a library 383 import std.algorithm : filter, map, splitter; 384 import std.array : array; 385 import std.file : exists, getcwd; 386 import std.path : chainPath, dirName; 387 import std.range : chain, only, take; 388 import std.process : environment; 389 390 version(Windows) { 391 enum exeName = "dub.exe"; 392 enum pathSep = ';'; 393 } 394 else { 395 enum exeName = "dub"; 396 enum pathSep = ':'; 397 } 398 399 auto dubLocs = only( 400 getcwd().chainPath(exeName), 401 compilerBinary.dirName.chainPath(exeName), 402 ) 403 .take(compilerBinary.length ? 2 : 1) 404 .chain( 405 environment.get("PATH", "") 406 .splitter(pathSep) 407 .map!(p => p.chainPath(exeName)) 408 ) 409 .filter!exists; 410 411 enforce(!dubLocs.empty, "Could not find DUB executable"); 412 return NativePath(dubLocs.front.array); 413 } 414 } 415 416 417 version(DubUseCurl) { 418 void setupHTTPClient(ref HTTP conn, uint timeout) 419 { 420 static if( is(typeof(&conn.verifyPeer)) ) 421 conn.verifyPeer = false; 422 423 auto proxy = environment.get("http_proxy", null); 424 if (proxy.length) conn.proxy = proxy; 425 426 auto noProxy = environment.get("no_proxy", null); 427 if (noProxy.length) conn.handle.set(CurlOption.noproxy, noProxy); 428 429 conn.handle.set(CurlOption.encoding, ""); 430 if (timeout) { 431 // connection (TLS+TCP) times out after 8s 432 conn.handle.set(CurlOption.connecttimeout, timeout); 433 // transfers time out after 8s below 10 byte/s 434 conn.handle.set(CurlOption.low_speed_limit, 10); 435 conn.handle.set(CurlOption.low_speed_time, timeout); 436 } 437 438 conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)"); 439 440 enum CURL_NETRC_OPTIONAL = 1; 441 conn.handle.set(CurlOption.netrc, CURL_NETRC_OPTIONAL); 442 } 443 } 444 445 private string stripUTF8Bom(string str) 446 { 447 if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) 448 return str[3 ..$]; 449 return str; 450 } 451 452 private bool isNumber(string str) { 453 foreach (ch; str) 454 switch (ch) { 455 case '0': .. case '9': break; 456 default: return false; 457 } 458 return true; 459 } 460 461 private bool isHexNumber(string str) { 462 foreach (ch; str) 463 switch (ch) { 464 case '0': .. case '9': break; 465 case 'a': .. case 'f': break; 466 case 'A': .. case 'F': break; 467 default: return false; 468 } 469 return true; 470 } 471 472 /** 473 Get the closest match of $(D input) in the $(D array), where $(D distance) 474 is the maximum levenshtein distance allowed between the compared strings. 475 Returns $(D null) if no closest match is found. 476 */ 477 string getClosestMatch(string[] array, string input, size_t distance) 478 { 479 import std.algorithm : countUntil, map, levenshteinDistance; 480 import std.uni : toUpper; 481 482 auto distMap = array.map!(elem => 483 levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input)); 484 auto idx = distMap.countUntil!(a => a <= distance); 485 return (idx == -1) ? null : array[idx]; 486 } 487 488 /** 489 Searches for close matches to input in range. R must be a range of strings 490 Note: Sorts the strings range. Use std.range.indexed to avoid this... 491 */ 492 auto fuzzySearch(R)(R strings, string input){ 493 import std.algorithm : levenshteinDistance, schwartzSort, partition3; 494 import std.traits : isSomeString; 495 import std.range : ElementType; 496 497 static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang"); 498 immutable threshold = input.length / 4; 499 return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1] 500 .schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper)); 501 } 502 503 /** 504 If T is a bitfield-style enum, this function returns a string range 505 listing the names of all members included in the given value. 506 507 Example: 508 --------- 509 enum Bits { 510 none = 0, 511 a = 1<<0, 512 b = 1<<1, 513 c = 1<<2, 514 a_c = a | c, 515 } 516 517 assert( bitFieldNames(Bits.none).equals(["none"]) ); 518 assert( bitFieldNames(Bits.a).equals(["a"]) ); 519 assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) ); 520 --------- 521 */ 522 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T) 523 { 524 import std.algorithm : filter, map; 525 import std.conv : to; 526 import std.traits : EnumMembers; 527 528 return [ EnumMembers!(T) ] 529 .filter!(member => member==0? value==0 : (value & member) == member) 530 .map!(member => to!string(member)); 531 } 532 533 534 bool isIdentChar(dchar ch) 535 { 536 import std.ascii : isAlphaNum; 537 return isAlphaNum(ch) || ch == '_'; 538 } 539 540 string stripDlangSpecialChars(string s) 541 { 542 import std.array : appender; 543 auto ret = appender!string(); 544 foreach(ch; s) 545 ret.put(isIdentChar(ch) ? ch : '_'); 546 return ret.data; 547 } 548 549 string determineModuleName(BuildSettings settings, NativePath file, NativePath base_path) 550 { 551 import std.algorithm : map; 552 import std.array : array; 553 import std.range : walkLength, chain; 554 555 assert(base_path.absolute); 556 if (!file.absolute) file = base_path ~ file; 557 558 size_t path_skip = 0; 559 foreach (ipath; chain(settings.importPaths, settings.cImportPaths).map!(p => NativePath(p))) { 560 if (!ipath.absolute) ipath = base_path ~ ipath; 561 assert(!ipath.empty); 562 if (file.startsWith(ipath) && ipath.bySegment.walkLength > path_skip) 563 path_skip = ipath.bySegment.walkLength; 564 } 565 566 auto mpath = file.bySegment.array[path_skip .. $]; 567 auto ret = appender!string; 568 569 //search for module keyword in file 570 string moduleName = getModuleNameFromFile(file.to!string); 571 572 if(moduleName.length) { 573 assert(moduleName.length > 0, "Wasn't this module name already checked? what"); 574 return moduleName; 575 } 576 577 //create module name from path 578 if (path_skip == 0) 579 { 580 import std.path; 581 ret ~= mpath[$-1].name.baseName(".d"); 582 } 583 else 584 { 585 foreach (i; 0 .. mpath.length) { 586 import std.path; 587 auto p = mpath[i].name; 588 if (p == "package.d") break ; 589 if (ret.data.length > 0) ret ~= "."; 590 if (i+1 < mpath.length) ret ~= p; 591 else ret ~= p.baseName(".d"); 592 } 593 } 594 595 assert(ret.data.length > 0, "A module name was expected to be computed, and none was."); 596 return ret.data; 597 } 598 599 /** 600 * Search for module keyword in D Code 601 * A primitive parser to skip comments and whitespace to get 602 * the module's name from the module declaration. 603 */ 604 string getModuleNameFromContent(string content) { 605 import std.ascii: isAlpha, isAlphaNum, isWhite; 606 import std.algorithm: among; 607 import core.exception: RangeError; 608 609 enum keyword = "module"; 610 611 size_t i = 0; 612 size_t startIndex = 0, endIndex = 0; 613 auto foundKeyword = false; 614 615 auto ch() { 616 return content[i]; 617 } 618 619 static bool isIdentChar(in char c) { 620 return !isWhite(c) && c != '/' && c != ';'; 621 } 622 623 try { 624 while(i < content.length) { 625 if(!foundKeyword && ch == keyword[0] && content[i .. i + keyword.length] == keyword) { 626 // -1 because the end of the loop will advance by 1 627 i += keyword.length - 1; 628 foundKeyword = true; 629 } 630 else if(ch == '/') { 631 ++i; 632 // line comment? 633 if(ch == '/') { 634 while(ch != '\n') 635 ++i; 636 } 637 // block comment? 638 else if(ch == '*') { 639 ++i; 640 while(ch != '*' || content[i + 1] != '/') 641 ++i; 642 ++i; // skip over closing '/' 643 } 644 // nested comment? 645 else if(ch == '+') { 646 ++i; 647 648 size_t level = 1; 649 650 while(level > 0) { 651 if(ch == '/') { 652 ++i; 653 if(ch == '+') { 654 ++i; 655 ++level; 656 } 657 } 658 if(ch == '+') { 659 ++i; 660 if(ch == '/') { 661 --level; 662 } else continue; 663 } 664 ++i; 665 } 666 } 667 } 668 else if(isIdentChar(ch) && foundKeyword) { 669 if(startIndex == 0) 670 startIndex = i; 671 ++i; // skip the first char of the name 672 while(isIdentChar(ch)) { 673 ++i; 674 } 675 // when we get here, either we're at the end of the module's identifier, 676 // or there are comments afterwards 677 if(endIndex == 0) { 678 endIndex = i; 679 } 680 if(!isIdentChar(ch)) 681 return content[startIndex .. endIndex]; 682 else continue; 683 } else if(!isIdentChar(ch) && foundKeyword && startIndex != 0) { 684 return content[startIndex .. endIndex]; 685 } 686 ++i; 687 } 688 return ""; 689 } catch(RangeError) { 690 return ""; 691 } 692 } 693 694 unittest { 695 assert(getModuleNameFromContent("") == ""); 696 assert(getModuleNameFromContent("module myPackage.myModule;") == "myPackage.myModule", getModuleNameFromContent("module myPackage.myModule;")); 697 assert(getModuleNameFromContent("module \t\n myPackage.myModule \t\r\n;") == "myPackage.myModule"); 698 assert(getModuleNameFromContent("// foo\nmodule bar;") == "bar"); 699 assert(getModuleNameFromContent("/*\nfoo\n*/\nmodule bar;") == "bar"); 700 assert(getModuleNameFromContent("/+\nfoo\n+/\nmodule bar;") == "bar"); 701 assert(getModuleNameFromContent("/***\nfoo\n***/\nmodule bar;") == "bar"); 702 assert(getModuleNameFromContent("/+++\nfoo\n+++/\nmodule bar;") == "bar"); 703 assert(getModuleNameFromContent("// module foo;\nmodule bar;") == "bar"); 704 assert(getModuleNameFromContent("/* module foo; */\nmodule bar;") == "bar"); 705 assert(getModuleNameFromContent("/+ module foo; +/\nmodule bar;") == "bar"); 706 assert(getModuleNameFromContent("/+ /+ module foo; +/ +/\nmodule bar;") == "bar"); 707 assert(getModuleNameFromContent("// module foo;\nmodule bar; // module foo;") == "bar"); 708 709 assert(getModuleNameFromContent("// module foo;\nmodule// module foo;\nbar//module foo;\n;// module foo;") == "bar"); 710 711 assert(getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;") == "bar", getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;")); 712 assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar;") == "bar"); 713 assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar/++/;") == "bar"); 714 assert(getModuleNameFromContent("/*\nmodule sometest;\n*/\n\nmodule fakemath;\n") == "fakemath"); 715 assert(getModuleNameFromContent("module foo_bar;") == "foo_bar"); 716 assert(getModuleNameFromContent("module _foo_bar;") == "_foo_bar"); 717 assert(getModuleNameFromContent("/++ ++/\nmodule foo;") == "foo"); 718 assert(getModuleNameFromContent("module pokémon;") == "pokémon"); 719 assert(getModuleNameFromContent("module éclair;") == "éclair"); 720 assert(getModuleNameFromContent("/** module foo*/ module bar;") == "bar"); 721 assert(getModuleNameFromContent("/* / module foo*/ module bar;") == "bar"); 722 723 assert(getModuleNameFromContent("module modules.foo;") == "modules.foo"); 724 } 725 726 /** 727 * Search for module keyword in file 728 */ 729 string getModuleNameFromFile(string filePath) { 730 if (!filePath.exists) 731 { 732 return null; 733 } 734 string fileContent = filePath.readText; 735 736 logDiagnostic("Get module name from path: %s", filePath); 737 return getModuleNameFromContent(fileContent); 738 } 739 740 /** 741 * Compare two instances of the same type for equality, 742 * providing a rich error message on failure. 743 * 744 * This function will recurse into composite types (struct, AA, arrays) 745 * and compare element / member wise, taking opEquals into account, 746 * to provide the most accurate reason why comparison failed. 747 */ 748 void deepCompare (T) ( 749 in T result, in T expected, string file = __FILE__, size_t line = __LINE__) 750 { 751 deepCompareImpl!T(result, expected, T.stringof, file, line); 752 } 753 754 void deepCompareImpl (T) ( 755 in T result, in T expected, string path, string file, size_t line) 756 { 757 static if (is(T == struct) && !is(typeof(T.init.opEquals(T.init)) : bool)) 758 { 759 static foreach (idx; 0 .. T.tupleof.length) 760 deepCompareImpl(result.tupleof[idx], expected.tupleof[idx], 761 format("%s.%s", path, __traits(identifier, T.tupleof[idx])), 762 file, line); 763 } 764 else static if (is(T : KeyT[ValueT], KeyT, ValueT)) 765 { 766 if (result.length != expected.length) 767 throw new Exception( 768 format("%s: AA has different number of entries (%s != %s): %s != %s", 769 path, result.length, expected.length, result, expected), 770 file, line); 771 foreach (key, value; expected) 772 { 773 if (auto ptr = key in result) 774 deepCompareImpl(*ptr, value, format("%s[%s]", path, key), file, line); 775 else 776 throw new Exception( 777 format("Expected key %s[%s] not present in result. %s != %s", 778 path, key, result, expected), file, line); 779 } 780 } 781 else if (result != expected) { 782 static if (is(T == struct) && is(typeof(T.init.opEquals(T.init)) : bool)) 783 path ~= ".opEquals"; 784 throw new Exception( 785 format("%s: result != expected: %s != %s", path, result, expected), 786 file, line); 787 } 788 } 789 790 /** Filters a forward range with the given predicate and returns a prefix range. 791 792 This function filters elements in-place, as opposed to returning a new 793 range. This can be particularly useful when working with arrays, as this 794 does not require any memory allocations. 795 796 This function guarantees that `pred` is called exactly once per element and 797 deterministically destroys any elements for which `pred` returns `false`. 798 */ 799 auto filterInPlace(alias pred, R)(R elems) 800 if (isForwardRange!R) 801 { 802 import std.algorithm.mutation : move; 803 804 R telems = elems.save; 805 bool any_removed = false; 806 size_t nret = 0; 807 foreach (ref el; elems.save) { 808 if (pred(el)) { 809 if (any_removed) move(el, telems.front); 810 telems.popFront(); 811 nret++; 812 } else any_removed = true; 813 } 814 return elems.takeExactly(nret); 815 } 816 817 /// 818 unittest { 819 int[] arr = [1, 2, 3, 4, 5, 6, 7, 8]; 820 821 arr = arr.filterInPlace!(e => e % 2 == 0); 822 assert(arr == [2, 4, 6, 8]); 823 824 arr = arr.filterInPlace!(e => e < 5); 825 assert(arr == [2, 4]); 826 }