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; 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 static if (__VERSION__ > 2075) 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 44 auto path = getTempDir() ~ (prefix ~ "-" ~ randomUUID.toString() ~ extension); 45 temporary_files ~= path; 46 return path; 47 } 48 49 /** 50 Obtain a lock for a file at the given path. If the file cannot be locked 51 within the given duration, an exception is thrown. The file will be created 52 if it does not yet exist. Deleting the file is not safe as another process 53 could create a new file with the same name. 54 The returned lock will get unlocked upon destruction. 55 56 Params: 57 path = path to file that gets locked 58 timeout = duration after which locking failed 59 Returns: 60 The locked file or an Exception on timeout. 61 */ 62 auto lockFile(string path, Duration timeout) 63 { 64 import core.thread : Thread; 65 import std.datetime, std.stdio : File; 66 import std.algorithm : move; 67 68 // Just a wrapper to hide (and destruct) the locked File. 69 static struct LockFile 70 { 71 // The Lock can't be unlinked as someone could try to lock an already 72 // opened fd while a new file with the same name gets created. 73 // Exclusive filesystem locks (O_EXCL, mkdir) could be deleted but 74 // aren't automatically freed when a process terminates, see #1149. 75 private File f; 76 } 77 78 auto file = File(path, "w"); 79 auto t0 = Clock.currTime(); 80 auto dur = 1.msecs; 81 while (true) 82 { 83 if (file.tryLock()) 84 return LockFile(move(file)); 85 enforce(Clock.currTime() - t0 < timeout, "Failed to lock '"~path~"'."); 86 if (dur < 1024.msecs) // exponentially increase sleep time 87 dur *= 2; 88 Thread.sleep(dur); 89 } 90 } 91 92 static ~this() 93 { 94 foreach (path; temporary_files) 95 { 96 auto spath = path.toNativeString(); 97 if (spath.exists) 98 std.file.remove(spath); 99 } 100 } 101 102 bool isEmptyDir(NativePath p) { 103 foreach(DirEntry e; dirEntries(p.toNativeString(), SpanMode.shallow)) 104 return false; 105 return true; 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 Json jsonFromZip(NativePath zip, string filename) { 128 import std.zip : ZipArchive; 129 auto f = openFile(zip, FileMode.read); 130 ubyte[] b = new ubyte[cast(size_t)f.size]; 131 f.rawRead(b); 132 f.close(); 133 auto archive = new ZipArchive(b); 134 auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename])); 135 return parseJsonString(text, zip.toNativeString~"/"~filename); 136 } 137 138 void writeJsonFile(NativePath path, Json json) 139 { 140 auto f = openFile(path, FileMode.createTrunc); 141 scope(exit) f.close(); 142 f.writePrettyJsonString(json); 143 } 144 145 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file 146 void atomicWriteJsonFile(NativePath path, Json json) 147 { 148 import std.random : uniform; 149 auto tmppath = path.parentPath ~ format("%s.%s.tmp", path.head, uniform(0, int.max)); 150 auto f = openFile(tmppath, FileMode.createTrunc); 151 scope (failure) { 152 f.close(); 153 removeFile(tmppath); 154 } 155 f.writePrettyJsonString(json); 156 f.close(); 157 if (existsFile(path)) removeFile(path); 158 moveFile(tmppath, path); 159 } 160 161 bool isPathFromZip(string p) { 162 enforce(p.length > 0); 163 return p[$-1] == '/'; 164 } 165 166 bool existsDirectory(NativePath path) { 167 if( !existsFile(path) ) return false; 168 auto fi = getFileInfo(path); 169 return fi.isDirectory; 170 } 171 172 void runCommand(string command, string[string] env = null) 173 { 174 runCommands((&command)[0 .. 1], env); 175 } 176 177 void runCommands(in string[] commands, string[string] env = null) 178 { 179 import std.stdio : stdin, stdout, stderr, File; 180 181 version(Windows) enum nullFile = "NUL"; 182 else version(Posix) enum nullFile = "/dev/null"; 183 else static assert(0); 184 185 auto childStdout = stdout; 186 auto childStderr = stderr; 187 auto config = Config.retainStdout | Config.retainStderr; 188 189 // Disable child's stdout/stderr depending on LogLevel 190 auto logLevel = getLogLevel(); 191 if(logLevel >= LogLevel.warn) 192 childStdout = File(nullFile, "w"); 193 if(logLevel >= LogLevel.none) 194 childStderr = File(nullFile, "w"); 195 196 foreach(cmd; commands){ 197 logDiagnostic("Running %s", cmd); 198 Pid pid; 199 pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config); 200 auto exitcode = pid.wait(); 201 enforce(exitcode == 0, "Command failed with exit code "~to!string(exitcode)); 202 } 203 } 204 205 version(DubUseCurl) { 206 /++ 207 Exception thrown on HTTP request failures, e.g. 404 Not Found. 208 +/ 209 static if (__VERSION__ <= 2075) class HTTPStatusException : CurlException 210 { 211 /++ 212 Params: 213 status = The HTTP status code. 214 msg = The message for the exception. 215 file = The file where the exception occurred. 216 line = The line number where the exception occurred. 217 next = The previous exception in the chain of exceptions, if any. 218 +/ 219 @safe pure nothrow 220 this( 221 int status, 222 string msg, 223 string file = __FILE__, 224 size_t line = __LINE__, 225 Throwable next = null) 226 { 227 this.status = status; 228 super(msg, file, line, next); 229 } 230 231 int status; /// The HTTP status code 232 } 233 } else version (Have_vibe_d_http) { 234 public import vibe.http.common : HTTPStatusException; 235 } 236 237 /** 238 Downloads a file from the specified URL. 239 240 Any redirects will be followed until the actual file resource is reached or if the redirection 241 limit of 10 is reached. Note that only HTTP(S) is currently supported. 242 243 The download times out if a connection cannot be established within 244 `timeout` ms, or if the average transfer rate drops below 10 bytes / s for 245 more than `timeout` seconds. Pass `0` as `timeout` to disable both timeout 246 mechanisms. 247 248 Note: Timeouts are only implemented when curl is used (DubUseCurl). 249 */ 250 void download(string url, string filename, uint timeout = 8) 251 { 252 version(DubUseCurl) { 253 auto conn = HTTP(); 254 setupHTTPClient(conn, timeout); 255 logDebug("Storing %s...", url); 256 static if (__VERSION__ <= 2075) 257 { 258 try 259 std.net.curl.download(url, filename, conn); 260 catch (CurlException e) 261 { 262 if (e.msg.canFind("404")) 263 throw new HTTPStatusException(404, e.msg); 264 throw e; 265 } 266 } 267 else 268 { 269 std.net.curl.download(url, filename, conn); 270 // workaround https://issues.dlang.org/show_bug.cgi?id=18318 271 auto sl = conn.statusLine; 272 logDebug("Download %s %s", url, sl); 273 if (sl.code / 100 != 2) 274 throw new HTTPStatusException(sl.code, 275 "Downloading %s failed with %d (%s).".format(url, sl.code, sl.reason)); 276 } 277 } else version (Have_vibe_d_http) { 278 import vibe.inet.urltransfer; 279 vibe.inet.urltransfer.download(url, filename); 280 } else assert(false); 281 } 282 /// ditto 283 void download(URL url, NativePath filename, uint timeout = 8) 284 { 285 download(url.toString(), filename.toNativeString(), timeout); 286 } 287 /// ditto 288 ubyte[] download(string url, uint timeout = 8) 289 { 290 version(DubUseCurl) { 291 auto conn = HTTP(); 292 setupHTTPClient(conn, timeout); 293 logDebug("Getting %s...", url); 294 static if (__VERSION__ <= 2075) 295 { 296 try 297 return cast(ubyte[])get(url, conn); 298 catch (CurlException e) 299 { 300 if (e.msg.canFind("404")) 301 throw new HTTPStatusException(404, e.msg); 302 throw e; 303 } 304 } 305 else 306 return cast(ubyte[])get(url, conn); 307 } else version (Have_vibe_d_http) { 308 import vibe.inet.urltransfer; 309 import vibe.stream.operations; 310 ubyte[] ret; 311 vibe.inet.urltransfer.download(url, (scope input) { ret = input.readAll(); }); 312 return ret; 313 } else assert(false); 314 } 315 /// ditto 316 ubyte[] download(URL url, uint timeout = 8) 317 { 318 return download(url.toString(), timeout); 319 } 320 321 /// Returns the current DUB version in semantic version format 322 string getDUBVersion() 323 { 324 import dub.version_; 325 import std.array : split, join; 326 // convert version string to valid SemVer format 327 auto verstr = dubVersion; 328 if (verstr.startsWith("v")) verstr = verstr[1 .. $]; 329 auto parts = verstr.split("-"); 330 if (parts.length >= 3) { 331 // detect GIT commit suffix 332 if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber()) 333 verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-"); 334 } 335 return verstr; 336 } 337 338 version(DubUseCurl) { 339 void setupHTTPClient(ref HTTP conn, uint timeout) 340 { 341 static if( is(typeof(&conn.verifyPeer)) ) 342 conn.verifyPeer = false; 343 344 auto proxy = environment.get("http_proxy", null); 345 if (proxy.length) conn.proxy = proxy; 346 347 auto noProxy = environment.get("no_proxy", null); 348 if (noProxy.length) conn.handle.set(CurlOption.noproxy, noProxy); 349 350 conn.handle.set(CurlOption.encoding, ""); 351 if (timeout) { 352 // connection (TLS+TCP) times out after 8s 353 conn.handle.set(CurlOption.connecttimeout, timeout); 354 // transfers time out after 8s below 10 byte/s 355 conn.handle.set(CurlOption.low_speed_limit, 10); 356 conn.handle.set(CurlOption.low_speed_time, 5); 357 } 358 359 conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)"); 360 } 361 } 362 363 string stripUTF8Bom(string str) 364 { 365 if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) 366 return str[3 ..$]; 367 return str; 368 } 369 370 private bool isNumber(string str) { 371 foreach (ch; str) 372 switch (ch) { 373 case '0': .. case '9': break; 374 default: return false; 375 } 376 return true; 377 } 378 379 private bool isHexNumber(string str) { 380 foreach (ch; str) 381 switch (ch) { 382 case '0': .. case '9': break; 383 case 'a': .. case 'f': break; 384 case 'A': .. case 'F': break; 385 default: return false; 386 } 387 return true; 388 } 389 390 /** 391 Get the closest match of $(D input) in the $(D array), where $(D distance) 392 is the maximum levenshtein distance allowed between the compared strings. 393 Returns $(D null) if no closest match is found. 394 */ 395 string getClosestMatch(string[] array, string input, size_t distance) 396 { 397 import std.algorithm : countUntil, map, levenshteinDistance; 398 import std.uni : toUpper; 399 400 auto distMap = array.map!(elem => 401 levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input)); 402 auto idx = distMap.countUntil!(a => a <= distance); 403 return (idx == -1) ? null : array[idx]; 404 } 405 406 /** 407 Searches for close matches to input in range. R must be a range of strings 408 Note: Sorts the strings range. Use std.range.indexed to avoid this... 409 */ 410 auto fuzzySearch(R)(R strings, string input){ 411 import std.algorithm : levenshteinDistance, schwartzSort, partition3; 412 import std.traits : isSomeString; 413 import std.range : ElementType; 414 415 static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang"); 416 immutable threshold = input.length / 4; 417 return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1] 418 .schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper)); 419 } 420 421 /** 422 If T is a bitfield-style enum, this function returns a string range 423 listing the names of all members included in the given value. 424 425 Example: 426 --------- 427 enum Bits { 428 none = 0, 429 a = 1<<0, 430 b = 1<<1, 431 c = 1<<2, 432 a_c = a | c, 433 } 434 435 assert( bitFieldNames(Bits.none).equals(["none"]) ); 436 assert( bitFieldNames(Bits.a).equals(["a"]) ); 437 assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) ); 438 --------- 439 */ 440 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T) 441 { 442 import std.algorithm : filter, map; 443 import std.conv : to; 444 import std.traits : EnumMembers; 445 446 return [ EnumMembers!(T) ] 447 .filter!(member => member==0? value==0 : (value & member) == member) 448 .map!(member => to!string(member)); 449 } 450 451 452 bool isIdentChar(dchar ch) 453 { 454 import std.ascii : isAlphaNum; 455 return isAlphaNum(ch) || ch == '_'; 456 } 457 458 string stripDlangSpecialChars(string s) 459 { 460 import std.array : appender; 461 auto ret = appender!string(); 462 foreach(ch; s) 463 ret.put(isIdentChar(ch) ? ch : '_'); 464 return ret.data; 465 } 466 467 string determineModuleName(BuildSettings settings, NativePath file, NativePath base_path) 468 { 469 import std.algorithm : map; 470 import std.array : array; 471 import std.range : walkLength; 472 473 assert(base_path.absolute); 474 if (!file.absolute) file = base_path ~ file; 475 476 size_t path_skip = 0; 477 foreach (ipath; settings.importPaths.map!(p => NativePath(p))) { 478 if (!ipath.absolute) ipath = base_path ~ ipath; 479 assert(!ipath.empty); 480 if (file.startsWith(ipath) && ipath.bySegment.walkLength > path_skip) 481 path_skip = ipath.bySegment.walkLength; 482 } 483 484 enforce(path_skip > 0, 485 format("Source file '%s' not found in any import path.", file.toNativeString())); 486 487 auto mpath = file.bySegment.array[path_skip .. $]; 488 auto ret = appender!string; 489 490 //search for module keyword in file 491 string moduleName = getModuleNameFromFile(file.to!string); 492 493 if(moduleName.length) return moduleName; 494 495 //create module name from path 496 foreach (i; 0 .. mpath.length) { 497 import std.path; 498 auto p = mpath[i].toString(); 499 if (p == "package.d") break; 500 if (i > 0) ret ~= "."; 501 if (i+1 < mpath.length) ret ~= p; 502 else ret ~= p.baseName(".d"); 503 } 504 505 return ret.data; 506 } 507 508 /** 509 * Search for module keyword in D Code 510 */ 511 string getModuleNameFromContent(string content) { 512 import std.regex; 513 import std.string; 514 515 content = content.strip; 516 if (!content.length) return null; 517 518 static bool regex_initialized = false; 519 static Regex!char comments_pattern, module_pattern; 520 521 if (!regex_initialized) { 522 comments_pattern = regex(`//[^\r\n]*\r?\n?|/\*.*?\*/|/\+.*\+/`, "g"); 523 module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g"); 524 regex_initialized = true; 525 } 526 527 content = replaceAll(content, comments_pattern, " "); 528 auto result = matchFirst(content, module_pattern); 529 530 if (!result.empty) return result[1]; 531 532 return null; 533 } 534 535 unittest { 536 assert(getModuleNameFromContent("") == ""); 537 assert(getModuleNameFromContent("module myPackage.myModule;") == "myPackage.myModule"); 538 assert(getModuleNameFromContent("module \t\n myPackage.myModule \t\r\n;") == "myPackage.myModule"); 539 assert(getModuleNameFromContent("// foo\nmodule bar;") == "bar"); 540 assert(getModuleNameFromContent("/*\nfoo\n*/\nmodule bar;") == "bar"); 541 assert(getModuleNameFromContent("/+\nfoo\n+/\nmodule bar;") == "bar"); 542 assert(getModuleNameFromContent("/***\nfoo\n***/\nmodule bar;") == "bar"); 543 assert(getModuleNameFromContent("/+++\nfoo\n+++/\nmodule bar;") == "bar"); 544 assert(getModuleNameFromContent("// module foo;\nmodule bar;") == "bar"); 545 assert(getModuleNameFromContent("/* module foo; */\nmodule bar;") == "bar"); 546 assert(getModuleNameFromContent("/+ module foo; +/\nmodule bar;") == "bar"); 547 assert(getModuleNameFromContent("/+ /+ module foo; +/ +/\nmodule bar;") == "bar"); 548 assert(getModuleNameFromContent("// module foo;\nmodule bar; // module foo;") == "bar"); 549 assert(getModuleNameFromContent("// module foo;\nmodule// module foo;\nbar//module foo;\n;// module foo;") == "bar"); 550 assert(getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;") == "bar", getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;")); 551 assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar;") == "bar"); 552 //assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar/++/;") == "bar"); // nested comments require a context-free parser! 553 } 554 555 /** 556 * Search for module keyword in file 557 */ 558 string getModuleNameFromFile(string filePath) { 559 string fileContent = filePath.readText; 560 561 logDiagnostic("Get module name from path: " ~ filePath); 562 return getModuleNameFromContent(fileContent); 563 }