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 // todo: cleanup imports. 18 import core.thread; 19 import std.algorithm : startsWith; 20 import std.array; 21 import std.conv; 22 import std.exception; 23 import std.file; 24 import std.process; 25 import std.string; 26 import std.traits : isIntegral; 27 import std.typecons; 28 import std.zip; 29 version(DubUseCurl) import std.net.curl; 30 31 32 private Path[] temporary_files; 33 34 Path getTempDir() 35 { 36 return Path(std.file.tempDir()); 37 } 38 39 Path getTempFile(string prefix, string extension = null) 40 { 41 import std.uuid : randomUUID; 42 43 auto path = getTempDir() ~ (prefix ~ "-" ~ randomUUID.toString() ~ extension); 44 temporary_files ~= path; 45 return path; 46 } 47 48 // lockfile based on atomic mkdir 49 struct LockFile 50 { 51 bool opCast(T:bool)() { return !!path; } 52 ~this() { if (path) rmdir(path); } 53 string path; 54 } 55 56 auto tryLockFile(string path) 57 { 58 import std.file; 59 if (collectException(mkdir(path))) 60 return LockFile(null); 61 return LockFile(path); 62 } 63 64 auto lockFile(string path, Duration wait) 65 { 66 import std.datetime, std.file; 67 auto t0 = Clock.currTime(); 68 auto dur = 1.msecs; 69 while (true) 70 { 71 if (!collectException(mkdir(path))) 72 return LockFile(path); 73 enforce(Clock.currTime() - t0 < wait, "Failed to lock '"~path~"'."); 74 if (dur < 1024.msecs) // exponentially increase sleep time 75 dur *= 2; 76 Thread.sleep(dur); 77 } 78 } 79 80 static ~this() 81 { 82 foreach (path; temporary_files) 83 { 84 auto spath = path.toNativeString(); 85 if (spath.exists) 86 std.file.remove(spath); 87 } 88 } 89 90 bool isEmptyDir(Path p) { 91 foreach(DirEntry e; dirEntries(p.toNativeString(), SpanMode.shallow)) 92 return false; 93 return true; 94 } 95 96 bool isWritableDir(Path p, bool create_if_missing = false) 97 { 98 import std.random; 99 auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max)); 100 if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString()); 101 try openFile(fname, FileMode.createTrunc).close(); 102 catch (Exception) return false; 103 remove(fname.toNativeString()); 104 return true; 105 } 106 107 Json jsonFromFile(Path file, bool silent_fail = false) { 108 if( silent_fail && !existsFile(file) ) return Json.emptyObject; 109 auto f = openFile(file.toNativeString(), FileMode.read); 110 scope(exit) f.close(); 111 auto text = stripUTF8Bom(cast(string)f.readAll()); 112 return parseJsonString(text, file.toNativeString()); 113 } 114 115 Json jsonFromZip(Path zip, string filename) { 116 auto f = openFile(zip, FileMode.read); 117 ubyte[] b = new ubyte[cast(size_t)f.size]; 118 f.rawRead(b); 119 f.close(); 120 auto archive = new ZipArchive(b); 121 auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename])); 122 return parseJsonString(text, zip.toNativeString~"/"~filename); 123 } 124 125 void writeJsonFile(Path path, Json json) 126 { 127 auto f = openFile(path, FileMode.createTrunc); 128 scope(exit) f.close(); 129 f.writePrettyJsonString(json); 130 } 131 132 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file 133 void atomicWriteJsonFile(Path path, Json json) 134 { 135 import std.random : uniform; 136 auto tmppath = path[0 .. $-1] ~ format("%s.%s.tmp", path.head, uniform(0, int.max)); 137 auto f = openFile(tmppath, FileMode.createTrunc); 138 scope (failure) { 139 f.close(); 140 removeFile(tmppath); 141 } 142 f.writePrettyJsonString(json); 143 f.close(); 144 if (existsFile(path)) removeFile(path); 145 moveFile(tmppath, path); 146 } 147 148 bool isPathFromZip(string p) { 149 enforce(p.length > 0); 150 return p[$-1] == '/'; 151 } 152 153 bool existsDirectory(Path path) { 154 if( !existsFile(path) ) return false; 155 auto fi = getFileInfo(path); 156 return fi.isDirectory; 157 } 158 159 void runCommand(string command, string[string] env = null) 160 { 161 runCommands((&command)[0 .. 1], env); 162 } 163 164 void runCommands(in string[] commands, string[string] env = null) 165 { 166 import std.stdio : stdin, stdout, stderr, File; 167 168 version(Windows) enum nullFile = "NUL"; 169 else version(Posix) enum nullFile = "/dev/null"; 170 else static assert(0); 171 172 auto childStdout = stdout; 173 auto childStderr = stderr; 174 auto config = Config.retainStdout | Config.retainStderr; 175 176 // Disable child's stdout/stderr depending on LogLevel 177 auto logLevel = getLogLevel(); 178 if(logLevel >= LogLevel.warn) 179 childStdout = File(nullFile, "w"); 180 if(logLevel >= LogLevel.none) 181 childStderr = File(nullFile, "w"); 182 183 foreach(cmd; commands){ 184 logDiagnostic("Running %s", cmd); 185 Pid pid; 186 pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config); 187 auto exitcode = pid.wait(); 188 enforce(exitcode == 0, "Command failed with exit code "~to!string(exitcode)); 189 } 190 } 191 192 /** 193 Downloads a file from the specified URL. 194 195 Any redirects will be followed until the actual file resource is reached or if the redirection 196 limit of 10 is reached. Note that only HTTP(S) is currently supported. 197 */ 198 void download(string url, string filename) 199 { 200 version(DubUseCurl) { 201 auto conn = HTTP(); 202 setupHTTPClient(conn); 203 logDebug("Storing %s...", url); 204 std.net.curl.download(url, filename, conn); 205 enforce(conn.statusLine.code < 400, 206 format("Failed to download %s: %s %s", 207 url, conn.statusLine.code, conn.statusLine.reason)); 208 } else version (Have_vibe_d) { 209 import vibe.inet.urltransfer; 210 vibe.inet.urltransfer.download(url, filename); 211 } else assert(false); 212 } 213 /// ditto 214 void download(URL url, Path filename) 215 { 216 download(url.toString(), filename.toNativeString()); 217 } 218 /// ditto 219 ubyte[] download(string url) 220 { 221 version(DubUseCurl) { 222 auto conn = HTTP(); 223 setupHTTPClient(conn); 224 logDebug("Getting %s...", url); 225 auto ret = cast(ubyte[])get(url, conn); 226 enforce(conn.statusLine.code < 400, 227 format("Failed to GET %s: %s %s", 228 url, conn.statusLine.code, conn.statusLine.reason)); 229 return ret; 230 } else version (Have_vibe_d) { 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 ubyte[] download(URL url) 240 { 241 return download(url.toString()); 242 } 243 244 /// Returns the current DUB version in semantic version format 245 string getDUBVersion() 246 { 247 import dub.version_; 248 // convert version string to valid SemVer format 249 auto verstr = dubVersion; 250 if (verstr.startsWith("v")) verstr = verstr[1 .. $]; 251 auto parts = verstr.split("-"); 252 if (parts.length >= 3) { 253 // detect GIT commit suffix 254 if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber()) 255 verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-"); 256 } 257 return verstr; 258 } 259 260 version(DubUseCurl) { 261 void setupHTTPClient(ref HTTP conn) 262 { 263 static if( is(typeof(&conn.verifyPeer)) ) 264 conn.verifyPeer = false; 265 266 auto proxy = environment.get("http_proxy", null); 267 if (proxy.length) conn.proxy = proxy; 268 269 conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)"); 270 } 271 } 272 273 string stripUTF8Bom(string str) 274 { 275 if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] ) 276 return str[3 ..$]; 277 return str; 278 } 279 280 private bool isNumber(string str) { 281 foreach (ch; str) 282 switch (ch) { 283 case '0': .. case '9': break; 284 default: return false; 285 } 286 return true; 287 } 288 289 private bool isHexNumber(string str) { 290 foreach (ch; str) 291 switch (ch) { 292 case '0': .. case '9': break; 293 case 'a': .. case 'f': break; 294 case 'A': .. case 'F': break; 295 default: return false; 296 } 297 return true; 298 } 299 300 /** 301 Get the closest match of $(D input) in the $(D array), where $(D distance) 302 is the maximum levenshtein distance allowed between the compared strings. 303 Returns $(D null) if no closest match is found. 304 */ 305 string getClosestMatch(string[] array, string input, size_t distance) 306 { 307 import std.algorithm : countUntil, map, levenshteinDistance; 308 import std.uni : toUpper; 309 310 auto distMap = array.map!(elem => 311 levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input)); 312 auto idx = distMap.countUntil!(a => a <= distance); 313 return (idx == -1) ? null : array[idx]; 314 } 315 316 /** 317 Searches for close matches to input in range. R must be a range of strings 318 Note: Sorts the strings range. Use std.range.indexed to avoid this... 319 */ 320 auto fuzzySearch(R)(R strings, string input){ 321 import std.algorithm : levenshteinDistance, schwartzSort, partition3; 322 import std.traits : isSomeString; 323 import std.range : ElementType; 324 325 static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang"); 326 immutable threshold = input.length / 4; 327 return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1] 328 .schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper)); 329 } 330 331 /** 332 If T is a bitfield-style enum, this function returns a string range 333 listing the names of all members included in the given value. 334 335 Example: 336 --------- 337 enum Bits { 338 none = 0, 339 a = 1<<0, 340 b = 1<<1, 341 c = 1<<2, 342 a_c = a | c, 343 } 344 345 assert( bitFieldNames(Bits.none).equals(["none"]) ); 346 assert( bitFieldNames(Bits.a).equals(["a"]) ); 347 assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) ); 348 --------- 349 */ 350 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T) 351 { 352 import std.algorithm : filter, map; 353 import std.conv : to; 354 import std.traits : EnumMembers; 355 356 return [ EnumMembers!(T) ] 357 .filter!(member => member==0? value==0 : (value & member) == member) 358 .map!(member => to!string(member)); 359 } 360 361 362 bool isIdentChar(dchar ch) 363 { 364 import std.ascii : isAlphaNum; 365 return isAlphaNum(ch) || ch == '_'; 366 } 367 368 string stripDlangSpecialChars(string s) 369 { 370 import std.array : appender; 371 auto ret = appender!string(); 372 foreach(ch; s) 373 ret.put(isIdentChar(ch) ? ch : '_'); 374 return ret.data; 375 } 376 377 string determineModuleName(BuildSettings settings, Path file, Path base_path) 378 { 379 import std.algorithm : map; 380 381 assert(base_path.absolute); 382 if (!file.absolute) file = base_path ~ file; 383 384 size_t path_skip = 0; 385 foreach (ipath; settings.importPaths.map!(p => Path(p))) { 386 if (!ipath.absolute) ipath = base_path ~ ipath; 387 assert(!ipath.empty); 388 if (file.startsWith(ipath) && ipath.length > path_skip) 389 path_skip = ipath.length; 390 } 391 392 enforce(path_skip > 0, 393 format("Source file '%s' not found in any import path.", file.toNativeString())); 394 395 auto mpath = file[path_skip .. file.length]; 396 auto ret = appender!string; 397 398 //search for module keyword in file 399 string moduleName = getModuleNameFromFile(file.to!string); 400 401 if(moduleName.length) return moduleName; 402 403 //create module name from path 404 foreach (i; 0 .. mpath.length) { 405 import std.path; 406 auto p = mpath[i].toString(); 407 if (p == "package.d") break; 408 if (i > 0) ret ~= "."; 409 if (i+1 < mpath.length) ret ~= p; 410 else ret ~= p.baseName(".d"); 411 } 412 413 return ret.data; 414 } 415 416 /** 417 * Search for module keyword in D Code 418 */ 419 string getModuleNameFromContent(string content) { 420 import std.regex; 421 import std.string; 422 423 content = content.strip; 424 if (!content.length) return null; 425 426 static bool regex_initialized = false; 427 static Regex!char comments_pattern, module_pattern; 428 429 if (!regex_initialized) { 430 comments_pattern = regex(`(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|(//.*)`, "g"); 431 module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g"); 432 regex_initialized = true; 433 } 434 435 content = replaceAll(content, comments_pattern, ""); 436 auto result = matchFirst(content, module_pattern); 437 438 string moduleName; 439 if(!result.empty) moduleName = result.front; 440 441 if (moduleName.length >= 7) moduleName = moduleName[7..$-1]; 442 443 return moduleName; 444 } 445 446 unittest { 447 //test empty string 448 string name = getModuleNameFromContent(""); 449 assert(name == "", "can't get module name from empty string"); 450 451 //test simple name 452 name = getModuleNameFromContent("module myPackage.myModule;"); 453 assert(name == "myPackage.myModule", "can't parse module name"); 454 455 //test if it can ignore module inside comments 456 name = getModuleNameFromContent("/** 457 module fakePackage.fakeModule; 458 */ 459 module myPackage.myModule;"); 460 461 assert(name == "myPackage.myModule", "can't parse module name"); 462 463 name = getModuleNameFromContent("//module fakePackage.fakeModule; 464 module myPackage.myModule;"); 465 466 assert(name == "myPackage.myModule", "can't parse module name"); 467 } 468 469 /** 470 * Search for module keyword in file 471 */ 472 string getModuleNameFromFile(string filePath) { 473 string fileContent = filePath.readText; 474 475 logDiagnostic("Get module name from path: " ~ filePath); 476 return getModuleNameFromContent(fileContent); 477 }