1 /** 2 Management of packages on the local computer. 3 4 Copyright: © 2012-2013 rejectedsoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig, Matthias Dondorff 7 */ 8 module dub.packagemanager; 9 10 import dub.dependency; 11 import dub.internal.utils; 12 import dub.internal.vibecompat.core.file; 13 import dub.internal.vibecompat.core.log; 14 import dub.internal.vibecompat.data.json; 15 import dub.internal.vibecompat.inet.path; 16 import dub.package_; 17 18 import std.algorithm : countUntil, filter, sort, canFind; 19 import std.array; 20 import std.conv; 21 import std.digest.sha; 22 import std.encoding : sanitize; 23 import std.exception; 24 import std.file; 25 import std.string; 26 import std.zip; 27 28 29 enum JournalJsonFilename = "journal.json"; 30 enum LocalPackagesFilename = "local-packages.json"; 31 32 33 private struct Repository { 34 Path path; 35 Path packagePath; 36 Path[] searchPath; 37 Package[] localPackages; 38 39 this(Path path) 40 { 41 this.path = path; 42 this.packagePath = path ~"packages/"; 43 } 44 } 45 46 enum LocalPackageType { 47 user, 48 system 49 } 50 51 /// The PackageManager can retrieve present packages and get / remove 52 /// packages. 53 class PackageManager { 54 private { 55 Repository[LocalPackageType] m_repositories; 56 Path[] m_searchPath; 57 Package[][string] m_packages; 58 Package[] m_temporaryPackages; 59 } 60 61 this(Path user_path, Path system_path) 62 { 63 m_repositories[LocalPackageType.user] = Repository(user_path); 64 m_repositories[LocalPackageType.system] = Repository(system_path); 65 refresh(true); 66 } 67 68 @property void searchPath(Path[] paths) { m_searchPath = paths.dup; refresh(false); } 69 @property const(Path)[] searchPath() const { return m_searchPath; } 70 71 @property const(Path)[] completeSearchPath() 72 const { 73 auto ret = appender!(Path[])(); 74 ret.put(m_searchPath); 75 ret.put(m_repositories[LocalPackageType.user].searchPath); 76 ret.put(m_repositories[LocalPackageType.user].packagePath); 77 ret.put(m_repositories[LocalPackageType.system].searchPath); 78 ret.put(m_repositories[LocalPackageType.system].packagePath); 79 return ret.data; 80 } 81 82 Package getPackage(string name, Version ver) 83 { 84 foreach( p; getPackageIterator(name) ) 85 if( p.ver == ver ) 86 return p; 87 return null; 88 } 89 90 Package getPackage(string name, string ver, Path in_path) 91 { 92 return getPackage(name, Version(ver), in_path); 93 } 94 Package getPackage(string name, Version ver, Path in_path) 95 { 96 foreach( p; getPackageIterator(name) ) 97 if (p.ver == ver && p.path.startsWith(in_path)) 98 return p; 99 return null; 100 } 101 102 Package getPackage(string name, string ver) 103 { 104 foreach (ep; getPackageIterator(name)) { 105 if (ep.vers == ver) 106 return ep; 107 } 108 return null; 109 } 110 111 Package getFirstPackage(string name) 112 { 113 foreach (ep; getPackageIterator(name)) 114 return ep; 115 return null; 116 } 117 118 Package getPackage(Path path) 119 { 120 foreach (p; getPackageIterator()) 121 if (!p.basePackage && p.path == path) 122 return p; 123 auto p = new Package(path); 124 m_temporaryPackages ~= p; 125 return p; 126 } 127 128 Package getBestPackage(string name, string version_spec) 129 { 130 return getBestPackage(name, Dependency(version_spec)); 131 } 132 133 Package getBestPackage(string name, Dependency version_spec) 134 { 135 Package ret; 136 foreach( p; getPackageIterator(name) ) 137 if( version_spec.matches(p.ver) && (!ret || p.ver > ret.ver) ) 138 ret = p; 139 return ret; 140 } 141 142 /** Determines if a package is managed by DUB. 143 144 Managed packages can be upgraded and uninstalled. 145 */ 146 bool isManagedPackage(Package pack) 147 const { 148 auto ppath = pack.basePackage.path; 149 foreach (rep; m_repositories) { 150 auto rpath = rep.packagePath; 151 if (ppath.startsWith(rpath)) 152 return true; 153 } 154 return false; 155 } 156 157 int delegate(int delegate(ref Package)) getPackageIterator() 158 { 159 int iterator(int delegate(ref Package) del) 160 { 161 int handlePackage(Package p) { 162 if (auto ret = del(p)) return ret; 163 foreach (sp; p.subPackages) 164 if (auto ret = del(sp)) 165 return ret; 166 return 0; 167 } 168 169 foreach (tp; m_temporaryPackages) 170 if (auto ret = handlePackage(tp)) return ret; 171 172 // first search local packages 173 foreach (tp; LocalPackageType.min .. LocalPackageType.max+1) 174 foreach (p; m_repositories[cast(LocalPackageType)tp].localPackages) 175 if (auto ret = handlePackage(p)) return ret; 176 177 // and then all packages gathered from the search path 178 foreach( pl; m_packages ) 179 foreach( v; pl ) 180 if( auto ret = handlePackage(v) ) 181 return ret; 182 return 0; 183 } 184 185 return &iterator; 186 } 187 188 int delegate(int delegate(ref Package)) getPackageIterator(string name) 189 { 190 int iterator(int delegate(ref Package) del) 191 { 192 foreach (p; getPackageIterator()) 193 if (p.name == name) 194 if (auto ret = del(p)) return ret; 195 return 0; 196 } 197 198 return &iterator; 199 } 200 201 /// Extracts the package supplied as a path to it's zip file to the 202 /// destination and sets a version field in the package description. 203 Package storeFetchedPackage(Path zip_file_path, Json package_info, Path destination) 204 { 205 auto package_name = package_info.name.get!string(); 206 auto package_version = package_info["version"].get!string(); 207 auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $]; 208 209 logDiagnostic("Placing package '%s' version '%s' to location '%s' from file '%s'", 210 package_name, package_version, destination.toNativeString(), zip_file_path.toNativeString()); 211 212 if( existsFile(destination) ){ 213 throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", package_name, package_version, destination)); 214 } 215 216 // open zip file 217 ZipArchive archive; 218 { 219 logDebug("Opening file %s", zip_file_path); 220 auto f = openFile(zip_file_path, FileMode.Read); 221 scope(exit) f.close(); 222 archive = new ZipArchive(f.readAll()); 223 } 224 225 logDebug("Extracting from zip."); 226 227 // In a github zip, the actual contents are in a subfolder 228 Path zip_prefix; 229 auto json_file = PathEntry(PackageJsonFilename); 230 foreach(ArchiveMember am; archive.directory) { 231 auto path = Path(am.name); 232 if (path.length == 2 && path.head == json_file && path.length) { 233 zip_prefix = path[0 .. $-1]; 234 break; 235 } 236 } 237 238 logDebug("zip root folder: %s", zip_prefix); 239 240 Path getCleanedPath(string fileName) { 241 auto path = Path(fileName); 242 if(zip_prefix != Path() && !path.startsWith(zip_prefix)) return Path(); 243 return path[zip_prefix.length..path.length]; 244 } 245 246 // extract & place 247 mkdirRecurse(destination.toNativeString()); 248 auto journal = new Journal; 249 logDiagnostic("Copying all files..."); 250 int countFiles = 0; 251 foreach(ArchiveMember a; archive.directory) { 252 auto cleanedPath = getCleanedPath(a.name); 253 if(cleanedPath.empty) continue; 254 auto dst_path = destination~cleanedPath; 255 256 logDebug("Creating %s", cleanedPath); 257 if( dst_path.endsWithSlash ){ 258 if( !existsDirectory(dst_path) ) 259 mkdirRecurse(dst_path.toNativeString()); 260 journal.add(Journal.Entry(Journal.Type.Directory, cleanedPath)); 261 } else { 262 if( !existsDirectory(dst_path.parentPath) ) 263 mkdirRecurse(dst_path.parentPath.toNativeString()); 264 auto dstFile = openFile(dst_path, FileMode.CreateTrunc); 265 scope(exit) dstFile.close(); 266 dstFile.put(archive.expand(a)); 267 journal.add(Journal.Entry(Journal.Type.RegularFile, cleanedPath)); 268 ++countFiles; 269 } 270 } 271 logDiagnostic("%s file(s) copied.", to!string(countFiles)); 272 273 // overwrite package.json (this one includes a version field) 274 Json pi = jsonFromFile(destination~PackageJsonFilename); 275 pi["name"] = toLower(pi["name"].get!string()); 276 pi["version"] = package_info["version"]; 277 writeJsonFile(destination~PackageJsonFilename, pi); 278 279 // Write journal 280 logDebug("Saving retrieval action journal..."); 281 journal.add(Journal.Entry(Journal.Type.RegularFile, Path(JournalJsonFilename))); 282 journal.save(destination ~ JournalJsonFilename); 283 284 if( existsFile(destination~PackageJsonFilename) ) 285 logInfo("%s is present with version %s", package_name, package_version); 286 287 auto pack = new Package(destination); 288 289 m_packages[package_name] ~= pack; 290 291 return pack; 292 } 293 294 /// Removes the given the package. 295 void remove(in Package pack) 296 { 297 logDebug("Remove %s, version %s, path '%s'", pack.name, pack.vers, pack.path); 298 enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path."); 299 300 // remove package from repositories' list 301 bool found = false; 302 bool removeFrom(Package[] packs, in Package pack) { 303 auto packPos = countUntil!("a.path == b.path")(packs, pack); 304 if(packPos != -1) { 305 packs = std.algorithm.remove(packs, packPos); 306 return true; 307 } 308 return false; 309 } 310 foreach(repo; m_repositories) { 311 if(removeFrom(repo.localPackages, pack)) { 312 found = true; 313 break; 314 } 315 } 316 if(!found) { 317 foreach(packsOfId; m_packages) { 318 if(removeFrom(packsOfId, pack)) { 319 found = true; 320 break; 321 } 322 } 323 } 324 enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path)); 325 326 // delete package files physically 327 logDebug("Looking up journal"); 328 auto journalFile = pack.path~JournalJsonFilename; 329 if( !existsFile(journalFile) ) 330 throw new Exception("Removal failed, no retrieval journal found for '"~pack.name~"'. Please remove manually."); 331 332 auto packagePath = pack.path; 333 auto journal = new Journal(journalFile); 334 logDebug("Erasing files"); 335 foreach( Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.RegularFile)(journal.entries)) { 336 logDebug("Deleting file '%s'", e.relFilename); 337 auto absFile = pack.path~e.relFilename; 338 if(!existsFile(absFile)) { 339 logWarn("Previously retrieved file not found for removal: '%s'", absFile); 340 continue; 341 } 342 343 removeFile(absFile); 344 } 345 346 logDiagnostic("Erasing directories"); 347 Path[] allPaths; 348 foreach(Journal.Entry e; filter!((Journal.Entry a) => a.type == Journal.Type.Directory)(journal.entries)) 349 allPaths ~= pack.path~e.relFilename; 350 sort!("a.length>b.length")(allPaths); // sort to erase deepest paths first 351 foreach(Path p; allPaths) { 352 logDebug("Deleting folder '%s'", p); 353 if( !existsFile(p) || !isDir(p.toNativeString()) || !isEmptyDir(p) ) { 354 logError("Alien files found, directory is not empty or is not a directory: '%s'", p); 355 continue; 356 } 357 rmdir(p.toNativeString()); 358 } 359 360 // Erase .dub folder, this is completely erased. 361 auto dubDir = (pack.path ~ ".dub/").toNativeString(); 362 enforce(!existsFile(dubDir) || isDir(dubDir), ".dub should be a directory, but is a file."); 363 if(existsFile(dubDir) && isDir(dubDir)) { 364 logDebug(".dub directory found, removing directory including content."); 365 rmdirRecurse(dubDir); 366 } 367 368 logDebug("About to delete root folder for package '%s'.", pack.path); 369 if(!isEmptyDir(pack.path)) 370 throw new Exception("Alien files found in '"~pack.path.toNativeString()~"', needs to be deleted manually."); 371 372 rmdir(pack.path.toNativeString()); 373 logInfo("Removed package: '"~pack.name~"'"); 374 } 375 376 Package addLocalPackage(in Path path, in Version ver, LocalPackageType type) 377 { 378 Package[]* packs = &m_repositories[type].localPackages; 379 auto info = jsonFromFile(path ~ PackageJsonFilename, false); 380 string name; 381 if( "name" !in info ) info["name"] = path.head.toString(); 382 info["version"] = ver.toString(); 383 384 // don't double-add packages 385 foreach( p; *packs ){ 386 if( p.path == path ){ 387 enforce(p.ver == ver, "Adding local twice with different versions is not allowed."); 388 logInfo("Package is already registered: %s (version: %s)", p.name, p.ver); 389 return p; 390 } 391 } 392 393 auto pack = new Package(info, path); 394 395 *packs ~= pack; 396 397 writeLocalPackageList(type); 398 399 logInfo("Registered package: %s (version: %s)", pack.name, pack.ver); 400 return pack; 401 } 402 403 void removeLocalPackage(in Path path, LocalPackageType type) 404 { 405 Package[]* packs = &m_repositories[type].localPackages; 406 size_t[] to_remove; 407 foreach( i, entry; *packs ) 408 if( entry.path == path ) 409 to_remove ~= i; 410 enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString()); 411 412 string[Version] removed; 413 foreach_reverse( i; to_remove ) { 414 removed[(*packs)[i].ver] = (*packs)[i].name; 415 *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $]; 416 } 417 418 writeLocalPackageList(type); 419 420 foreach(ver, name; removed) 421 logInfo("Unregistered package: %s (version: %s)", name, ver); 422 } 423 424 Package getTemporaryPackage(Path path, Version ver) 425 { 426 foreach (p; m_temporaryPackages) 427 if (p.path == path) { 428 enforce(p.ver == ver, format("Package in %s is refrenced with two conflicting versions: %s vs %s", path.toNativeString(), p.ver, ver)); 429 return p; 430 } 431 432 auto info = jsonFromFile(path ~ PackageJsonFilename, false); 433 string name; 434 if( "name" !in info ) info["name"] = path.head.toString(); 435 info["version"] = ver.toString(); 436 437 auto pack = new Package(info, path); 438 m_temporaryPackages ~= pack; 439 return pack; 440 } 441 442 /// For the given type add another path where packages will be looked up. 443 void addSearchPath(Path path, LocalPackageType type) 444 { 445 m_repositories[type].searchPath ~= path; 446 writeLocalPackageList(type); 447 } 448 449 /// Removes a search path from the given type. 450 void removeSearchPath(Path path, LocalPackageType type) 451 { 452 m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array(); 453 writeLocalPackageList(type); 454 } 455 456 void refresh(bool refresh_existing_packages) 457 { 458 // load locally defined packages 459 void scanLocalPackages(LocalPackageType type) 460 { 461 Path list_path = m_repositories[type].packagePath; 462 Package[] packs; 463 Path[] paths; 464 try { 465 auto local_package_file = list_path ~ LocalPackagesFilename; 466 logDiagnostic("Looking for local package map at %s", local_package_file.toNativeString()); 467 if( !existsFile(local_package_file) ) return; 468 logDiagnostic("Try to load local package map at %s", local_package_file.toNativeString()); 469 auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename); 470 enforce(packlist.type == Json.Type.array, LocalPackagesFilename~" must contain an array."); 471 foreach( pentry; packlist ){ 472 try { 473 auto name = pentry.name.get!string(); 474 auto path = Path(pentry.path.get!string()); 475 if (name == "*") { 476 paths ~= path; 477 } else { 478 auto ver = pentry["version"].get!string(); 479 auto info = Json.emptyObject; 480 if( existsFile(path ~ PackageJsonFilename) ) info = jsonFromFile(path ~ PackageJsonFilename); 481 if( "name" in info && info.name.get!string() != name ) 482 logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, info.name.get!string()); 483 info.name = name; 484 info["version"] = ver; 485 486 Package pp; 487 if (!refresh_existing_packages) 488 foreach (p; m_repositories[type].localPackages) 489 if (p.path == path) { 490 pp = p; 491 break; 492 } 493 if (!pp) pp = new Package(info, path); 494 packs ~= pp; 495 } 496 } catch( Exception e ){ 497 logWarn("Error adding local package: %s", e.msg); 498 } 499 } 500 } catch( Exception e ){ 501 logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg); 502 } 503 m_repositories[type].localPackages = packs; 504 m_repositories[type].searchPath = paths; 505 } 506 scanLocalPackages(LocalPackageType.system); 507 scanLocalPackages(LocalPackageType.user); 508 509 Package[][string] old_packages = m_packages; 510 511 // rescan the system and user package folder 512 void scanPackageFolder(Path path) 513 { 514 if( path.existsDirectory() ){ 515 logDebug("iterating dir %s", path.toNativeString()); 516 try foreach( pdir; iterateDirectory(path) ){ 517 logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name); 518 if( !pdir.isDirectory ) continue; 519 auto pack_path = path ~ pdir.name; 520 if( !existsFile(pack_path ~ PackageJsonFilename) ) continue; 521 Package p; 522 try { 523 if (!refresh_existing_packages) 524 foreach (plist; old_packages) 525 foreach (pp; plist) 526 if (pp.path == pack_path) { 527 p = pp; 528 break; 529 } 530 if (!p) p = new Package(pack_path); 531 m_packages[p.name] ~= p; 532 } catch( Exception e ){ 533 logError("Failed to load package in %s: %s", pack_path, e.msg); 534 logDiagnostic("Full error: %s", e.toString().sanitize()); 535 } 536 } 537 catch(Exception e) logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString()); 538 } 539 } 540 541 m_packages = null; 542 foreach (p; this.completeSearchPath) 543 scanPackageFolder(p); 544 } 545 546 alias ubyte[] Hash; 547 /// Generates a hash value for a given package. 548 /// Some files or folders are ignored during the generation (like .dub and 549 /// .svn folders) 550 Hash hashPackage(Package pack) 551 { 552 string[] ignored_directories = [".git", ".dub", ".svn"]; 553 // something from .dub_ignore or what? 554 string[] ignored_files = []; 555 SHA1 sha1; 556 foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) { 557 if(file.isDir && ignored_directories.canFind(Path(file.name).head.toString())) 558 continue; 559 else if(ignored_files.canFind(Path(file.name).head.toString())) 560 continue; 561 562 sha1.put(cast(ubyte[])Path(file.name).head.toString()); 563 if(file.isDir) { 564 logDebug("Hashed directory name %s", Path(file.name).head); 565 } 566 else { 567 sha1.put(openFile(Path(file.name)).readAll()); 568 logDebug("Hashed file contents from %s", Path(file.name).head); 569 } 570 } 571 auto hash = sha1.finish(); 572 logDebug("Project hash: %s", hash); 573 return hash[0..$]; 574 } 575 576 private void writeLocalPackageList(LocalPackageType type) 577 { 578 Json[] newlist; 579 foreach (p; m_repositories[type].searchPath) { 580 auto entry = Json.emptyObject; 581 entry.name = "*"; 582 entry.path = p.toNativeString(); 583 newlist ~= entry; 584 } 585 586 foreach (p; m_repositories[type].localPackages) { 587 auto entry = Json.emptyObject; 588 entry["name"] = p.name; 589 entry["version"] = p.ver.toString(); 590 entry["path"] = p.path.toNativeString(); 591 newlist ~= entry; 592 } 593 594 Path path = m_repositories[type].packagePath; 595 if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString()); 596 writeJsonFile(path ~ LocalPackagesFilename, Json(newlist)); 597 } 598 } 599 600 601 /** 602 Retrieval journal for later removal, keeping track of placed files 603 files. 604 Example Json: 605 { 606 "version": 1, 607 "files": { 608 "file1": "typeoffile1", 609 ... 610 } 611 } 612 */ 613 private class Journal { 614 private enum Version = 1; 615 616 enum Type { 617 RegularFile, 618 Directory, 619 Alien 620 } 621 622 struct Entry { 623 this( Type t, Path f ) { type = t; relFilename = f; } 624 Type type; 625 Path relFilename; 626 } 627 628 @property const(Entry[]) entries() const { return m_entries; } 629 630 this() {} 631 632 /// Initializes a Journal from a json file. 633 this(Path journalFile) { 634 auto jsonJournal = jsonFromFile(journalFile); 635 enforce(cast(int)jsonJournal["Version"] == Version, "Mismatched version: "~to!string(cast(int)jsonJournal["Version"]) ~ "vs. " ~to!string(Version)); 636 foreach(string file, type; jsonJournal["Files"]) 637 m_entries ~= Entry(to!Type(cast(string)type), Path(file)); 638 } 639 640 void add(Entry e) { 641 foreach(Entry ent; entries) { 642 if( e.relFilename == ent.relFilename ) { 643 enforce(e.type == ent.type, "Duplicate('"~to!string(e.relFilename)~"'), different types: "~to!string(e.type)~" vs. "~to!string(ent.type)); 644 return; 645 } 646 } 647 m_entries ~= e; 648 } 649 650 /// Save the current state to the path. 651 void save(Path path) { 652 Json jsonJournal = serialize(); 653 auto fileJournal = openFile(path, FileMode.CreateTrunc); 654 scope(exit) fileJournal.close(); 655 fileJournal.writePrettyJsonString(jsonJournal); 656 } 657 658 private Json serialize() const { 659 Json[string] files; 660 foreach(Entry e; m_entries) 661 files[to!string(e.relFilename)] = to!string(e.type); 662 Json[string] json; 663 json["Version"] = Version; 664 json["Files"] = files; 665 return Json(json); 666 } 667 668 private { 669 Entry[] m_entries; 670 } 671 }