1 /** 2 Management of packages on the local computer. 3 4 Copyright: © 2012-2016 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, remove; 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 /// The PackageManager can retrieve present packages and get / remove 30 /// packages. 31 class PackageManager { 32 private { 33 Repository[LocalPackageType] m_repositories; 34 Path[] m_searchPath; 35 Package[] m_packages; 36 Package[] m_temporaryPackages; 37 bool m_disableDefaultSearchPaths = false; 38 } 39 40 this(Path user_path, Path system_path, bool refresh_packages = true) 41 { 42 m_repositories[LocalPackageType.user] = Repository(user_path); 43 m_repositories[LocalPackageType.system] = Repository(system_path); 44 if (refresh_packages) refresh(true); 45 } 46 47 /** Gets/sets the list of paths to search for local packages. 48 */ 49 @property void searchPath(Path[] paths) 50 { 51 if (paths == m_searchPath) return; 52 m_searchPath = paths.dup; 53 refresh(false); 54 } 55 /// ditto 56 @property const(Path)[] searchPath() const { return m_searchPath; } 57 58 /** Disables searching DUB's predefined search paths. 59 */ 60 @property void disableDefaultSearchPaths(bool val) 61 { 62 if (val == m_disableDefaultSearchPaths) return; 63 m_disableDefaultSearchPaths = val; 64 refresh(true); 65 } 66 67 /** Returns the effective list of search paths, including default ones. 68 */ 69 @property const(Path)[] completeSearchPath() 70 const { 71 auto ret = appender!(Path[])(); 72 ret.put(m_searchPath); 73 if (!m_disableDefaultSearchPaths) { 74 ret.put(m_repositories[LocalPackageType.user].searchPath); 75 ret.put(m_repositories[LocalPackageType.user].packagePath); 76 ret.put(m_repositories[LocalPackageType.system].searchPath); 77 ret.put(m_repositories[LocalPackageType.system].packagePath); 78 } 79 return ret.data; 80 } 81 82 83 /** Looks up a specific package. 84 85 Looks up a package matching the given version/path in the set of 86 registered packages. The lookup order is done according the the 87 usual rules (see getPackageIterator). 88 89 Params: 90 name = The name of the package 91 ver = The exact version of the package to query 92 path = An exact path that the package must reside in. Note that 93 the package must still be registered in the package manager. 94 enable_overrides = Apply the local package override list before 95 returning a package (enabled by default) 96 97 Returns: 98 The matching package or null if no match was found. 99 */ 100 Package getPackage(string name, Version ver, bool enable_overrides = true) 101 { 102 if (enable_overrides) { 103 foreach (tp; [LocalPackageType.user, LocalPackageType.system]) 104 foreach (ovr; m_repositories[tp].overrides) 105 if (ovr.package_ == name && ovr.version_.matches(ver)) { 106 Package pack; 107 if (!ovr.targetPath.empty) pack = getPackage(name, ovr.targetPath); 108 else pack = getPackage(name, ovr.targetVersion, false); 109 if (pack) return pack; 110 111 logWarn("Package override %s %s -> %s %s doesn't reference an existing package.", 112 ovr.package_, ovr.version_, ovr.targetVersion, ovr.targetPath); 113 } 114 } 115 116 foreach (p; getPackageIterator(name)) 117 if (p.version_ == ver) 118 return p; 119 120 return null; 121 } 122 123 /// ditto 124 Package getPackage(string name, string ver, bool enable_overrides = true) 125 { 126 return getPackage(name, Version(ver), enable_overrides); 127 } 128 129 /// ditto 130 Package getPackage(string name, Version ver, Path path) 131 { 132 auto ret = getPackage(name, path); 133 if (!ret || ret.version_ != ver) return null; 134 return ret; 135 } 136 137 /// ditto 138 Package getPackage(string name, string ver, Path path) 139 { 140 return getPackage(name, Version(ver), path); 141 } 142 143 /// ditto 144 Package getPackage(string name, Path path) 145 { 146 foreach( p; getPackageIterator(name) ) 147 if (p.path.startsWith(path)) 148 return p; 149 return null; 150 } 151 152 153 /** Looks up the first package matching the given name. 154 */ 155 Package getFirstPackage(string name) 156 { 157 foreach (ep; getPackageIterator(name)) 158 return ep; 159 return null; 160 } 161 162 /** For a given package path, returns the corresponding package. 163 164 If the package is already loaded, a reference is returned. Otherwise 165 the package gets loaded and cached for the next call to this function. 166 167 Params: 168 path = Path to the root directory of the package 169 recipe_path = Optional path to the recipe file of the package 170 allow_sub_packages = Also return a sub package if it resides in the given folder 171 172 Returns: The packages loaded from the given path 173 Throws: Throws an exception if no package can be loaded 174 */ 175 Package getOrLoadPackage(Path path, Path recipe_path = Path.init, bool allow_sub_packages = false) 176 { 177 path.endsWithSlash = true; 178 foreach (p; getPackageIterator()) 179 if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path))) 180 return p; 181 auto pack = Package.load(path, recipe_path); 182 addPackages(m_temporaryPackages, pack); 183 return pack; 184 } 185 186 187 /** Searches for the latest version of a package matching the given dependency. 188 */ 189 Package getBestPackage(string name, Dependency version_spec, bool enable_overrides = true) 190 { 191 Package ret; 192 foreach (p; getPackageIterator(name)) 193 if (version_spec.matches(p.version_) && (!ret || p.version_ > ret.version_)) 194 ret = p; 195 196 if (enable_overrides && ret) { 197 if (auto ovr = getPackage(name, ret.version_)) 198 return ovr; 199 } 200 return ret; 201 } 202 203 /// ditto 204 Package getBestPackage(string name, string version_spec) 205 { 206 return getBestPackage(name, Dependency(version_spec)); 207 } 208 209 /** Gets the a specific sub package. 210 211 In contrast to `Package.getSubPackage`, this function supports path 212 based sub packages. 213 214 Params: 215 base_package = The package from which to get a sub package 216 sub_name = Name of the sub package (not prefixed with the base 217 package name) 218 silent_fail = If set to true, the function will return `null` if no 219 package is found. Otherwise will throw an exception. 220 221 */ 222 Package getSubPackage(Package base_package, string sub_name, bool silent_fail) 223 { 224 foreach (p; getPackageIterator(base_package.name~":"~sub_name)) 225 if (p.parentPackage is base_package) 226 return p; 227 enforce(silent_fail, "Sub package "~base_package.name~":"~sub_name~" doesn't exist."); 228 return null; 229 } 230 231 232 /** Determines if a package is managed by DUB. 233 234 Managed packages can be upgraded and removed. 235 */ 236 bool isManagedPackage(Package pack) 237 const { 238 auto ppath = pack.basePackage.path; 239 return isManagedPath(ppath); 240 } 241 242 /** Determines if a specifc path is within a DUB managed package folder. 243 244 By default, managed folders are "~/.dub/packages" and 245 "/var/lib/dub/packages". 246 */ 247 bool isManagedPath(Path path) 248 const { 249 foreach (rep; m_repositories) { 250 auto rpath = rep.packagePath; 251 if (path.startsWith(rpath)) 252 return true; 253 } 254 return false; 255 } 256 257 /** Enables iteration over all known local packages. 258 259 Returns: A delegate suitable for use with `foreach` is returned. 260 */ 261 int delegate(int delegate(ref Package)) getPackageIterator() 262 { 263 int iterator(int delegate(ref Package) del) 264 { 265 foreach (tp; m_temporaryPackages) 266 if (auto ret = del(tp)) return ret; 267 268 // first search local packages 269 foreach (tp; LocalPackageType.min .. LocalPackageType.max+1) 270 foreach (p; m_repositories[cast(LocalPackageType)tp].localPackages) 271 if (auto ret = del(p)) return ret; 272 273 // and then all packages gathered from the search path 274 foreach( p; m_packages ) 275 if( auto ret = del(p) ) 276 return ret; 277 return 0; 278 } 279 280 return &iterator; 281 } 282 283 /** Enables iteration over all known local packages with a certain name. 284 285 Returns: A delegate suitable for use with `foreach` is returned. 286 */ 287 int delegate(int delegate(ref Package)) getPackageIterator(string name) 288 { 289 int iterator(int delegate(ref Package) del) 290 { 291 foreach (p; getPackageIterator()) 292 if (p.name == name) 293 if (auto ret = del(p)) return ret; 294 return 0; 295 } 296 297 return &iterator; 298 } 299 300 301 /** Returns a list of all package overrides for the given scope. 302 */ 303 const(PackageOverride)[] getOverrides(LocalPackageType scope_) 304 const { 305 return m_repositories[scope_].overrides; 306 } 307 308 /** Adds a new override for the given package. 309 */ 310 void addOverride(LocalPackageType scope_, string package_, Dependency version_spec, Version target) 311 { 312 m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); 313 writeLocalPackageOverridesFile(scope_); 314 } 315 /// ditto 316 void addOverride(LocalPackageType scope_, string package_, Dependency version_spec, Path target) 317 { 318 m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); 319 writeLocalPackageOverridesFile(scope_); 320 } 321 322 /** Removes an existing package override. 323 */ 324 void removeOverride(LocalPackageType scope_, string package_, Dependency version_spec) 325 { 326 Repository* rep = &m_repositories[scope_]; 327 foreach (i, ovr; rep.overrides) { 328 if (ovr.package_ != package_ || ovr.version_ != version_spec) 329 continue; 330 rep.overrides = rep.overrides[0 .. i] ~ rep.overrides[i+1 .. $]; 331 writeLocalPackageOverridesFile(scope_); 332 return; 333 } 334 throw new Exception(format("No override exists for %s %s", package_, version_spec)); 335 } 336 337 /// Extracts the package supplied as a path to it's zip file to the 338 /// destination and sets a version field in the package description. 339 Package storeFetchedPackage(Path zip_file_path, Json package_info, Path destination) 340 { 341 auto package_name = package_info["name"].get!string; 342 auto package_version = package_info["version"].get!string; 343 auto clean_package_version = package_version[package_version.startsWith("~") ? 1 : 0 .. $]; 344 345 logDebug("Placing package '%s' version '%s' to location '%s' from file '%s'", 346 package_name, package_version, destination.toNativeString(), zip_file_path.toNativeString()); 347 348 if( existsFile(destination) ){ 349 throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", package_name, package_version, destination)); 350 } 351 352 // open zip file 353 ZipArchive archive; 354 { 355 logDebug("Opening file %s", zip_file_path); 356 auto f = openFile(zip_file_path, FileMode.read); 357 scope(exit) f.close(); 358 archive = new ZipArchive(f.readAll()); 359 } 360 361 logDebug("Extracting from zip."); 362 363 // In a github zip, the actual contents are in a subfolder 364 Path zip_prefix; 365 outer: foreach(ArchiveMember am; archive.directory) { 366 auto path = Path(am.name); 367 foreach (fil; packageInfoFiles) 368 if (path.length == 2 && path.head.toString == fil.filename) { 369 zip_prefix = path[0 .. $-1]; 370 break outer; 371 } 372 } 373 374 logDebug("zip root folder: %s", zip_prefix); 375 376 Path getCleanedPath(string fileName) { 377 auto path = Path(fileName); 378 if(zip_prefix != Path() && !path.startsWith(zip_prefix)) return Path(); 379 return path[zip_prefix.length..path.length]; 380 } 381 382 // extract & place 383 mkdirRecurse(destination.toNativeString()); 384 logDebug("Copying all files..."); 385 int countFiles = 0; 386 foreach(ArchiveMember a; archive.directory) { 387 auto cleanedPath = getCleanedPath(a.name); 388 if(cleanedPath.empty) continue; 389 auto dst_path = destination~cleanedPath; 390 391 logDebug("Creating %s", cleanedPath); 392 if( dst_path.endsWithSlash ){ 393 if( !existsDirectory(dst_path) ) 394 mkdirRecurse(dst_path.toNativeString()); 395 } else { 396 if( !existsDirectory(dst_path.parentPath) ) 397 mkdirRecurse(dst_path.parentPath.toNativeString()); 398 auto dstFile = openFile(dst_path, FileMode.createTrunc); 399 scope(exit) dstFile.close(); 400 dstFile.put(archive.expand(a)); 401 ++countFiles; 402 } 403 } 404 logDebug("%s file(s) copied.", to!string(countFiles)); 405 406 // overwrite dub.json (this one includes a version field) 407 auto pack = Package.load(destination, Path.init, null, package_info["version"].get!string); 408 409 if (pack.recipePath.head != defaultPackageFilename) 410 // Storeinfo saved a default file, this could be different to the file from the zip. 411 removeFile(pack.recipePath); 412 pack.storeInfo(); 413 addPackages(m_packages, pack); 414 return pack; 415 } 416 417 /// Removes the given the package. 418 void remove(in Package pack, bool force_remove) 419 { 420 logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path); 421 enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path."); 422 423 // remove package from repositories' list 424 bool found = false; 425 bool removeFrom(Package[] packs, in Package pack) { 426 auto packPos = countUntil!("a.path == b.path")(packs, pack); 427 if(packPos != -1) { 428 packs = .remove(packs, packPos); 429 return true; 430 } 431 return false; 432 } 433 foreach(repo; m_repositories) { 434 if(removeFrom(repo.localPackages, pack)) { 435 found = true; 436 break; 437 } 438 } 439 if(!found) 440 found = removeFrom(m_packages, pack); 441 enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path)); 442 443 logDebug("About to delete root folder for package '%s'.", pack.path); 444 rmdirRecurse(pack.path.toNativeString()); 445 logInfo("Removed package: '"~pack.name~"'"); 446 } 447 448 Package addLocalPackage(Path path, string verName, LocalPackageType type) 449 { 450 path.endsWithSlash = true; 451 auto pack = Package.load(path); 452 enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString()); 453 if (verName.length) 454 pack.version_ = Version(verName); 455 456 // don't double-add packages 457 Package[]* packs = &m_repositories[type].localPackages; 458 foreach (p; *packs) { 459 if (p.path == path) { 460 enforce(p.version_ == pack.version_, "Adding the same local package twice with differing versions is not allowed."); 461 logInfo("Package is already registered: %s (version: %s)", p.name, p.version_); 462 return p; 463 } 464 } 465 466 addPackages(*packs, pack); 467 468 writeLocalPackageList(type); 469 470 logInfo("Registered package: %s (version: %s)", pack.name, pack.version_); 471 return pack; 472 } 473 474 void removeLocalPackage(Path path, LocalPackageType type) 475 { 476 path.endsWithSlash = true; 477 478 Package[]* packs = &m_repositories[type].localPackages; 479 size_t[] to_remove; 480 foreach( i, entry; *packs ) 481 if( entry.path == path ) 482 to_remove ~= i; 483 enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString()); 484 485 string[Version] removed; 486 foreach_reverse( i; to_remove ) { 487 removed[(*packs)[i].version_] = (*packs)[i].name; 488 *packs = (*packs)[0 .. i] ~ (*packs)[i+1 .. $]; 489 } 490 491 writeLocalPackageList(type); 492 493 foreach(ver, name; removed) 494 logInfo("Deregistered package: %s (version: %s)", name, ver); 495 } 496 497 /// For the given type add another path where packages will be looked up. 498 void addSearchPath(Path path, LocalPackageType type) 499 { 500 m_repositories[type].searchPath ~= path; 501 writeLocalPackageList(type); 502 } 503 504 /// Removes a search path from the given type. 505 void removeSearchPath(Path path, LocalPackageType type) 506 { 507 m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array(); 508 writeLocalPackageList(type); 509 } 510 511 void refresh(bool refresh_existing_packages) 512 { 513 logDiagnostic("Refreshing local packages (refresh existing: %s)...", refresh_existing_packages); 514 515 // load locally defined packages 516 void scanLocalPackages(LocalPackageType type) 517 { 518 Path list_path = m_repositories[type].packagePath; 519 Package[] packs; 520 Path[] paths; 521 if (!m_disableDefaultSearchPaths) try { 522 auto local_package_file = list_path ~ LocalPackagesFilename; 523 logDiagnostic("Looking for local package map at %s", local_package_file.toNativeString()); 524 if( !existsFile(local_package_file) ) return; 525 logDiagnostic("Try to load local package map at %s", local_package_file.toNativeString()); 526 auto packlist = jsonFromFile(list_path ~ LocalPackagesFilename); 527 enforce(packlist.type == Json.Type.array, LocalPackagesFilename~" must contain an array."); 528 foreach( pentry; packlist ){ 529 try { 530 auto name = pentry["name"].get!string; 531 auto path = Path(pentry["path"].get!string); 532 if (name == "*") { 533 paths ~= path; 534 } else { 535 auto ver = Version(pentry["version"].get!string); 536 537 Package pp; 538 if (!refresh_existing_packages) { 539 foreach (p; m_repositories[type].localPackages) 540 if (p.path == path) { 541 pp = p; 542 break; 543 } 544 } 545 546 if (!pp) { 547 auto infoFile = Package.findPackageFile(path); 548 if (!infoFile.empty) pp = Package.load(path, infoFile); 549 else { 550 logWarn("Locally registered package %s %s was not found. Please run 'dub remove-local \"%s\"'.", 551 name, ver, path.toNativeString()); 552 auto info = Json.emptyObject; 553 info["name"] = name; 554 pp = new Package(info, path); 555 } 556 } 557 558 if (pp.name != name) 559 logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name); 560 pp.version_ = ver; 561 562 addPackages(packs, pp); 563 } 564 } catch( Exception e ){ 565 logWarn("Error adding local package: %s", e.msg); 566 } 567 } 568 } catch( Exception e ){ 569 logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg); 570 } 571 m_repositories[type].localPackages = packs; 572 m_repositories[type].searchPath = paths; 573 } 574 scanLocalPackages(LocalPackageType.system); 575 scanLocalPackages(LocalPackageType.user); 576 577 auto old_packages = m_packages; 578 579 // rescan the system and user package folder 580 void scanPackageFolder(Path path) 581 { 582 if( path.existsDirectory() ){ 583 logDebug("iterating dir %s", path.toNativeString()); 584 try foreach( pdir; iterateDirectory(path) ){ 585 logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name); 586 if (!pdir.isDirectory) continue; 587 588 auto pack_path = path ~ (pdir.name ~ "/"); 589 590 auto packageFile = Package.findPackageFile(pack_path); 591 592 if (isManagedPath(path) && packageFile.empty) { 593 // Search for a single directory within this directory which happen to be a prefix of pdir 594 // This is to support new folder structure installed over the ancient one. 595 foreach (subdir; iterateDirectory(path ~ (pdir.name ~ "/"))) 596 if (subdir.isDirectory && pdir.name.startsWith(subdir.name)) {// eg: package vibe-d will be in "vibe-d-x.y.z/vibe-d" 597 pack_path ~= subdir.name ~ "/"; 598 packageFile = Package.findPackageFile(pack_path); 599 break; 600 } 601 } 602 603 if (packageFile.empty) continue; 604 Package p; 605 try { 606 if (!refresh_existing_packages) 607 foreach (pp; old_packages) 608 if (pp.path == pack_path) { 609 p = pp; 610 break; 611 } 612 if (!p) p = Package.load(pack_path, packageFile); 613 addPackages(m_packages, p); 614 } catch( Exception e ){ 615 logError("Failed to load package in %s: %s", pack_path, e.msg); 616 logDiagnostic("Full error: %s", e.toString().sanitize()); 617 } 618 } 619 catch(Exception e) logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString()); 620 } 621 } 622 623 m_packages = null; 624 foreach (p; this.completeSearchPath) 625 scanPackageFolder(p); 626 627 void loadOverrides(LocalPackageType type) 628 { 629 m_repositories[type].overrides = null; 630 auto ovrfilepath = m_repositories[type].packagePath ~ LocalOverridesFilename; 631 if (existsFile(ovrfilepath)) { 632 foreach (entry; jsonFromFile(ovrfilepath)) { 633 PackageOverride ovr; 634 ovr.package_ = entry["name"].get!string; 635 ovr.version_ = Dependency(entry["version"].get!string); 636 if (auto pv = "targetVersion" in entry) ovr.targetVersion = Version(pv.get!string); 637 if (auto pv = "targetPath" in entry) ovr.targetPath = Path(pv.get!string); 638 m_repositories[type].overrides ~= ovr; 639 } 640 } 641 } 642 loadOverrides(LocalPackageType.user); 643 loadOverrides(LocalPackageType.system); 644 } 645 646 alias Hash = ubyte[]; 647 /// Generates a hash value for a given package. 648 /// Some files or folders are ignored during the generation (like .dub and 649 /// .svn folders) 650 Hash hashPackage(Package pack) 651 { 652 string[] ignored_directories = [".git", ".dub", ".svn"]; 653 // something from .dub_ignore or what? 654 string[] ignored_files = []; 655 SHA1 sha1; 656 foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) { 657 if(file.isDir && ignored_directories.canFind(Path(file.name).head.toString())) 658 continue; 659 else if(ignored_files.canFind(Path(file.name).head.toString())) 660 continue; 661 662 sha1.put(cast(ubyte[])Path(file.name).head.toString()); 663 if(file.isDir) { 664 logDebug("Hashed directory name %s", Path(file.name).head); 665 } 666 else { 667 sha1.put(openFile(Path(file.name)).readAll()); 668 logDebug("Hashed file contents from %s", Path(file.name).head); 669 } 670 } 671 auto hash = sha1.finish(); 672 logDebug("Project hash: %s", hash); 673 return hash[].dup; 674 } 675 676 private void writeLocalPackageList(LocalPackageType type) 677 { 678 Json[] newlist; 679 foreach (p; m_repositories[type].searchPath) { 680 auto entry = Json.emptyObject; 681 entry["name"] = "*"; 682 entry["path"] = p.toNativeString(); 683 newlist ~= entry; 684 } 685 686 foreach (p; m_repositories[type].localPackages) { 687 if (p.parentPackage) continue; // do not store sub packages 688 auto entry = Json.emptyObject; 689 entry["name"] = p.name; 690 entry["version"] = p.version_.toString(); 691 entry["path"] = p.path.toNativeString(); 692 newlist ~= entry; 693 } 694 695 Path path = m_repositories[type].packagePath; 696 if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString()); 697 writeJsonFile(path ~ LocalPackagesFilename, Json(newlist)); 698 } 699 700 private void writeLocalPackageOverridesFile(LocalPackageType type) 701 { 702 Json[] newlist; 703 foreach (ovr; m_repositories[type].overrides) { 704 auto jovr = Json.emptyObject; 705 jovr["name"] = ovr.package_; 706 jovr["version"] = ovr.version_.versionSpec; 707 if (!ovr.targetPath.empty) jovr["targetPath"] = ovr.targetPath.toNativeString(); 708 else jovr["targetVersion"] = ovr.targetVersion.toString(); 709 newlist ~= jovr; 710 } 711 auto path = m_repositories[type].packagePath; 712 if (!existsDirectory(path)) mkdirRecurse(path.toNativeString()); 713 writeJsonFile(path ~ LocalOverridesFilename, Json(newlist)); 714 } 715 716 /// Adds the package and scans for subpackages. 717 private void addPackages(ref Package[] dst_repos, Package pack) 718 const { 719 // Add the main package. 720 dst_repos ~= pack; 721 722 // Additionally to the internally defined subpackages, whose metadata 723 // is loaded with the main dub.json, load all externally defined 724 // packages after the package is available with all the data. 725 foreach (spr; pack.subPackages) { 726 Package sp; 727 728 if (spr.path.length) { 729 auto p = Path(spr.path); 730 p.normalize(); 731 enforce(!p.absolute, "Sub package paths must be sub paths of the parent package."); 732 auto path = pack.path ~ p; 733 if (!existsFile(path)) { 734 logError("Package %s declared a sub-package, definition file is missing: %s", pack.name, path.toNativeString()); 735 continue; 736 } 737 sp = Package.load(path, Path.init, pack); 738 } else sp = new Package(spr.recipe, pack.path, pack); 739 740 // Add the subpackage. 741 try { 742 dst_repos ~= sp; 743 } catch (Exception e) { 744 logError("Package '%s': Failed to load sub-package %s: %s", pack.name, 745 spr.path.length ? spr.path : spr.recipe.name, e.msg); 746 logDiagnostic("Full error: %s", e.toString().sanitize()); 747 } 748 } 749 } 750 } 751 752 struct PackageOverride { 753 string package_; 754 Dependency version_; 755 Version targetVersion; 756 Path targetPath; 757 758 this(string package_, Dependency version_, Version target_version) 759 { 760 this.package_ = package_; 761 this.version_ = version_; 762 this.targetVersion = target_version; 763 } 764 765 this(string package_, Dependency version_, Path target_path) 766 { 767 this.package_ = package_; 768 this.version_ = version_; 769 this.targetPath = target_path; 770 } 771 } 772 773 enum LocalPackageType { 774 user, 775 system 776 } 777 778 private enum LocalPackagesFilename = "local-packages.json"; 779 private enum LocalOverridesFilename = "local-overrides.json"; 780 781 782 private struct Repository { 783 Path path; 784 Path packagePath; 785 Path[] searchPath; 786 Package[] localPackages; 787 PackageOverride[] overrides; 788 789 this(Path path) 790 { 791 this.path = path; 792 this.packagePath = path ~"packages/"; 793 } 794 }