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.data.json; 14 import dub.internal.vibecompat.inet.path; 15 import dub.internal.logging; 16 import dub.package_; 17 import dub.recipe.io; 18 import configy.Exceptions; 19 public import configy.Read : StrictMode; 20 21 import std.algorithm : countUntil, filter, map, sort, canFind, remove; 22 import std.array; 23 import std.conv; 24 import std.digest.sha; 25 import std.encoding : sanitize; 26 import std.exception; 27 import std.file; 28 import std.range; 29 import std.string; 30 import std.sumtype; 31 import std.zip; 32 33 34 /// Indicates where a package has been or should be placed to. 35 public enum PlacementLocation { 36 /// Packages retrieved with 'local' will be placed in the current folder 37 /// using the package name as destination. 38 local, 39 /// Packages with 'userWide' will be placed in a folder accessible by 40 /// all of the applications from the current user. 41 user, 42 /// Packages retrieved with 'systemWide' will be placed in a shared folder, 43 /// which can be accessed by all users of the system. 44 system, 45 } 46 47 /// Converts a `PlacementLocation` to a string 48 public string toString (PlacementLocation loc) @safe pure nothrow @nogc 49 { 50 final switch (loc) { 51 case PlacementLocation.local: 52 return "Local"; 53 case PlacementLocation.user: 54 return "User"; 55 case PlacementLocation.system: 56 return "System"; 57 } 58 } 59 60 /// The PackageManager can retrieve present packages and get / remove 61 /// packages. 62 class PackageManager { 63 private { 64 /** 65 * The 'internal' location, for packages not attributable to a location. 66 * 67 * There are two uses for this: 68 * - In `bare` mode, the search paths are set at this scope, 69 * and packages gathered are stored in `localPackage`; 70 * - In the general case, any path-based or SCM-based dependency 71 * is loaded in `fromPath`; 72 */ 73 Location m_internal; 74 /** 75 * List of locations that are managed by this `PackageManager` 76 * 77 * The `PackageManager` can be instantiated either in 'bare' mode, 78 * in which case this array will be empty, or in the normal mode, 79 * this array will have 3 entries, matching values 80 * in the `PlacementLocation` enum. 81 * 82 * See_Also: `Location`, `PlacementLocation` 83 */ 84 Location[] m_repositories; 85 } 86 87 /** 88 Instantiate an instance with a single search path 89 90 This constructor is used when dub is invoked with the '--bar' CLI switch. 91 The instance will not look up the default repositories 92 (e.g. ~/.dub/packages), using only `path` instead. 93 94 Params: 95 path = Path of the single repository 96 */ 97 this(NativePath path) 98 { 99 this.m_internal.searchPath = [ path ]; 100 this.refresh(true); 101 } 102 103 this(NativePath package_path, NativePath user_path, NativePath system_path, bool refresh_packages = true) 104 { 105 m_repositories = [ 106 Location(package_path ~ ".dub/packages/"), 107 Location(user_path ~ "packages/"), 108 Location(system_path ~ "packages/")]; 109 110 if (refresh_packages) refresh(true); 111 } 112 113 /** Gets/sets the list of paths to search for local packages. 114 */ 115 @property void searchPath(NativePath[] paths) 116 { 117 if (paths == this.m_internal.searchPath) return; 118 this.m_internal.searchPath = paths.dup; 119 refresh(false); 120 } 121 /// ditto 122 @property const(NativePath)[] searchPath() const { return this.m_internal.searchPath; } 123 124 /** Returns the effective list of search paths, including default ones. 125 */ 126 deprecated("Use the `PackageManager` facilities instead") 127 @property const(NativePath)[] completeSearchPath() 128 const { 129 auto ret = appender!(const(NativePath)[])(); 130 ret.put(this.m_internal.searchPath); 131 foreach (ref repo; m_repositories) { 132 ret.put(repo.searchPath); 133 ret.put(repo.packagePath); 134 } 135 return ret.data; 136 } 137 138 /** Sets additional (read-only) package cache paths to search for packages. 139 140 Cache paths have the same structure as the default cache paths, such as 141 ".dub/packages/". 142 143 Note that previously set custom paths will be removed when setting this 144 property. 145 */ 146 @property void customCachePaths(NativePath[] custom_cache_paths) 147 { 148 import std.algorithm.iteration : map; 149 import std.array : array; 150 151 m_repositories.length = PlacementLocation.max+1; 152 m_repositories ~= custom_cache_paths.map!(p => Location(p)).array; 153 154 refresh(false); 155 } 156 157 158 /** Looks up a specific package. 159 160 Looks up a package matching the given version/path in the set of 161 registered packages. The lookup order is done according the the 162 usual rules (see getPackageIterator). 163 164 Params: 165 name = The name of the package 166 ver = The exact version of the package to query 167 path = An exact path that the package must reside in. Note that 168 the package must still be registered in the package manager. 169 enable_overrides = Apply the local package override list before 170 returning a package (enabled by default) 171 172 Returns: 173 The matching package or null if no match was found. 174 */ 175 Package getPackage(string name, Version ver, bool enable_overrides = true) 176 { 177 if (enable_overrides) { 178 foreach (ref repo; m_repositories) 179 foreach (ovr; repo.overrides) 180 if (ovr.package_ == name && ovr.source.matches(ver)) { 181 Package pack = ovr.target.match!( 182 (NativePath path) => getOrLoadPackage(path), 183 (Version vers) => getPackage(name, vers, false), 184 ); 185 if (pack) return pack; 186 187 ovr.target.match!( 188 (any) { 189 logWarn("Package override %s %s -> '%s' doesn't reference an existing package.", 190 ovr.package_, ovr.version_, any); 191 }, 192 ); 193 } 194 } 195 196 foreach (p; getPackageIterator(name)) 197 if (p.version_.matches(ver, isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard)) 198 return p; 199 200 return null; 201 } 202 203 /// ditto 204 deprecated("Use the overload that accepts a `Version` as second argument") 205 Package getPackage(string name, string ver, bool enable_overrides = true) 206 { 207 return getPackage(name, Version(ver), enable_overrides); 208 } 209 210 /// ditto 211 deprecated("Use the overload that takes a `PlacementLocation`") 212 Package getPackage(string name, Version ver, NativePath path) 213 { 214 foreach (p; getPackageIterator(name)) { 215 auto pvm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard; 216 if (p.version_.matches(ver, pvm) && p.path.startsWith(path)) 217 return p; 218 } 219 return null; 220 } 221 222 /// Ditto 223 Package getPackage(string name, Version ver, PlacementLocation loc) 224 { 225 // Bare mode 226 if (loc >= this.m_repositories.length) 227 return null; 228 return this.m_repositories[loc].lookup(name, ver); 229 } 230 231 /// ditto 232 deprecated("Use the overload that accepts a `Version` as second argument") 233 Package getPackage(string name, string ver, NativePath path) 234 { 235 return getPackage(name, Version(ver), path); 236 } 237 238 /// ditto 239 deprecated("Use another `PackageManager` API, open an issue if none suits you") 240 Package getPackage(string name, NativePath path) 241 { 242 foreach( p; getPackageIterator(name) ) 243 if (p.path.startsWith(path)) 244 return p; 245 return null; 246 } 247 248 249 /** Looks up the first package matching the given name. 250 */ 251 deprecated("Use `getBestPackage` instead") 252 Package getFirstPackage(string name) 253 { 254 foreach (ep; getPackageIterator(name)) 255 return ep; 256 return null; 257 } 258 259 /** Looks up the latest package matching the given name. 260 */ 261 deprecated("Use `getBestPackage` with `name, Dependency.any` instead") 262 Package getLatestPackage(string name) 263 { 264 Package pkg; 265 foreach (ep; getPackageIterator(name)) 266 if (pkg is null || pkg.version_ < ep.version_) 267 pkg = ep; 268 return pkg; 269 } 270 271 /** For a given package path, returns the corresponding package. 272 273 If the package is already loaded, a reference is returned. Otherwise 274 the package gets loaded and cached for the next call to this function. 275 276 Params: 277 path = NativePath to the root directory of the package 278 recipe_path = Optional path to the recipe file of the package 279 allow_sub_packages = Also return a sub package if it resides in the given folder 280 mode = Whether to issue errors, warning, or ignore unknown keys in dub.json 281 282 Returns: The packages loaded from the given path 283 Throws: Throws an exception if no package can be loaded 284 */ 285 Package getOrLoadPackage(NativePath path, NativePath recipe_path = NativePath.init, 286 bool allow_sub_packages = false, StrictMode mode = StrictMode.Ignore) 287 { 288 path.endsWithSlash = true; 289 foreach (p; this.m_internal.fromPath) 290 if (p.path == path && (!p.parentPackage || (allow_sub_packages && p.parentPackage.path != p.path))) 291 return p; 292 auto pack = Package.load(path, recipe_path, null, null, mode); 293 addPackages(this.m_internal.fromPath, pack); 294 return pack; 295 } 296 297 /** For a given SCM repository, returns the corresponding package. 298 299 An SCM repository is provided as its remote URL, the repository is cloned 300 and in the dependency speicfied commit is checked out. 301 302 If the target directory already exists, just returns the package 303 without cloning. 304 305 Params: 306 name = Package name 307 dependency = Dependency that contains the repository URL and a specific commit 308 309 Returns: 310 The package loaded from the given SCM repository or null if the 311 package couldn't be loaded. 312 */ 313 deprecated("Use the overload that accepts a `dub.dependency : Repository`") 314 Package loadSCMPackage(string name, Dependency dependency) 315 in { assert(!dependency.repository.empty); } 316 do { return this.loadSCMPackage(name, dependency.repository); } 317 318 /// Ditto 319 Package loadSCMPackage(string name, Repository repo) 320 in { assert(!repo.empty); } 321 do { 322 Package pack; 323 324 final switch (repo.kind) 325 { 326 case repo.Kind.git: 327 pack = loadGitPackage(name, repo); 328 } 329 if (pack !is null) { 330 addPackages(this.m_internal.fromPath, pack); 331 } 332 return pack; 333 } 334 335 private Package loadGitPackage(string name, in Repository repo) 336 { 337 import dub.internal.git : cloneRepository; 338 339 if (!repo.ref_.startsWith("~") && !repo.ref_.isGitHash) { 340 return null; 341 } 342 343 string gitReference = repo.ref_.chompPrefix("~"); 344 NativePath destination = this.getPackagePath(PlacementLocation.user, name, repo.ref_); 345 // For libraries leaking their import path 346 destination ~= name; 347 destination.endsWithSlash = true; 348 349 foreach (p; getPackageIterator(name)) { 350 if (p.path == destination) { 351 return p; 352 } 353 } 354 355 if (!cloneRepository(repo.remote, gitReference, destination.toNativeString())) { 356 return null; 357 } 358 359 return Package.load(destination); 360 } 361 362 /** 363 * Get the final destination a specific package needs to be stored in. 364 * 365 * See `Location.getPackagePath`. 366 */ 367 package(dub) NativePath getPackagePath (PlacementLocation base, string name, string vers) 368 { 369 assert(this.m_repositories.length == 3, "getPackagePath called in bare mode"); 370 return this.m_repositories[base].getPackagePath(name, vers); 371 } 372 373 /** Searches for the latest version of a package matching the given dependency. 374 */ 375 Package getBestPackage(string name, VersionRange range = VersionRange.Any) 376 { 377 return this.getBestPackage(name, Dependency(range)); 378 } 379 380 /// Ditto 381 Package getBestPackage(string name, Version vers) 382 { 383 return this.getBestPackage(name, VersionRange(vers, vers)); 384 } 385 386 /// Ditto 387 Package getBestPackage(string name, Dependency version_spec, bool enable_overrides = true) 388 { 389 Package ret; 390 foreach (p; getPackageIterator(name)) { 391 auto vmm = isManagedPackage(p) ? VersionMatchMode.strict : VersionMatchMode.standard; 392 if (version_spec.matches(p.version_, vmm) && (!ret || p.version_ > ret.version_)) 393 ret = p; 394 } 395 396 if (enable_overrides && ret) { 397 if (auto ovr = getPackage(name, ret.version_)) 398 return ovr; 399 } 400 return ret; 401 } 402 403 /// ditto 404 Package getBestPackage(string name, string version_spec) 405 { 406 return getBestPackage(name, Dependency(version_spec)); 407 } 408 409 /** Gets the a specific sub package. 410 411 In contrast to `Package.getSubPackage`, this function supports path 412 based sub packages. 413 414 Params: 415 base_package = The package from which to get a sub package 416 sub_name = Name of the sub package (not prefixed with the base 417 package name) 418 silent_fail = If set to true, the function will return `null` if no 419 package is found. Otherwise will throw an exception. 420 421 */ 422 Package getSubPackage(Package base_package, string sub_name, bool silent_fail) 423 { 424 foreach (p; getPackageIterator(base_package.name~":"~sub_name)) 425 if (p.parentPackage is base_package) 426 return p; 427 enforce(silent_fail, "Sub package \""~base_package.name~":"~sub_name~"\" doesn't exist."); 428 return null; 429 } 430 431 432 /** Determines if a package is managed by DUB. 433 434 Managed packages can be upgraded and removed. 435 */ 436 bool isManagedPackage(Package pack) 437 const { 438 auto ppath = pack.basePackage.path; 439 return isManagedPath(ppath); 440 } 441 442 /** Determines if a specific path is within a DUB managed package folder. 443 444 By default, managed folders are "~/.dub/packages" and 445 "/var/lib/dub/packages". 446 */ 447 bool isManagedPath(NativePath path) 448 const { 449 foreach (rep; m_repositories) { 450 NativePath rpath = rep.packagePath; 451 if (path.startsWith(rpath)) 452 return true; 453 } 454 return false; 455 } 456 457 /** Enables iteration over all known local packages. 458 459 Returns: A delegate suitable for use with `foreach` is returned. 460 */ 461 int delegate(int delegate(ref Package)) getPackageIterator() 462 { 463 int iterator(int delegate(ref Package) del) 464 { 465 // Search scope by priority, internal has the highest 466 foreach (p; this.m_internal.fromPath) 467 if (auto ret = del(p)) return ret; 468 foreach (p; this.m_internal.localPackages) 469 if (auto ret = del(p)) return ret; 470 471 foreach (ref repo; m_repositories) { 472 foreach (p; repo.localPackages) 473 if (auto ret = del(p)) return ret; 474 foreach (p; repo.fromPath) 475 if (auto ret = del(p)) return ret; 476 } 477 return 0; 478 } 479 480 return &iterator; 481 } 482 483 /** Enables iteration over all known local packages with a certain name. 484 485 Returns: A delegate suitable for use with `foreach` is returned. 486 */ 487 int delegate(int delegate(ref Package)) getPackageIterator(string name) 488 { 489 int iterator(int delegate(ref Package) del) 490 { 491 foreach (p; getPackageIterator()) 492 if (p.name == name) 493 if (auto ret = del(p)) return ret; 494 return 0; 495 } 496 497 return &iterator; 498 } 499 500 501 /** Returns a list of all package overrides for the given scope. 502 */ 503 deprecated(OverrideDepMsg) 504 const(PackageOverride)[] getOverrides(PlacementLocation scope_) 505 const { 506 return cast(typeof(return)) this.getOverrides_(scope_); 507 } 508 509 package(dub) const(PackageOverride_)[] getOverrides_(PlacementLocation scope_) 510 const { 511 return m_repositories[scope_].overrides; 512 } 513 514 /** Adds a new override for the given package. 515 */ 516 deprecated("Use the overload that accepts a `VersionRange` as 3rd argument") 517 void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, Version target) 518 { 519 m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); 520 m_repositories[scope_].writeOverrides(); 521 } 522 /// ditto 523 deprecated("Use the overload that accepts a `VersionRange` as 3rd argument") 524 void addOverride(PlacementLocation scope_, string package_, Dependency version_spec, NativePath target) 525 { 526 m_repositories[scope_].overrides ~= PackageOverride(package_, version_spec, target); 527 m_repositories[scope_].writeOverrides(); 528 } 529 530 /// Ditto 531 deprecated(OverrideDepMsg) 532 void addOverride(PlacementLocation scope_, string package_, VersionRange source, Version target) 533 { 534 this.addOverride_(scope_, package_, source, target); 535 } 536 /// ditto 537 deprecated(OverrideDepMsg) 538 void addOverride(PlacementLocation scope_, string package_, VersionRange source, NativePath target) 539 { 540 this.addOverride_(scope_, package_, source, target); 541 } 542 543 // Non deprecated version that is used by `commandline`. Do not use! 544 package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, Version target) 545 { 546 m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target); 547 m_repositories[scope_].writeOverrides(); 548 } 549 // Non deprecated version that is used by `commandline`. Do not use! 550 package(dub) void addOverride_(PlacementLocation scope_, string package_, VersionRange source, NativePath target) 551 { 552 m_repositories[scope_].overrides ~= PackageOverride_(package_, source, target); 553 m_repositories[scope_].writeOverrides(); 554 } 555 556 /** Removes an existing package override. 557 */ 558 deprecated("Use the overload that accepts a `VersionRange` as 3rd argument") 559 void removeOverride(PlacementLocation scope_, string package_, Dependency version_spec) 560 { 561 version_spec.visit!( 562 (VersionRange src) => this.removeOverride(scope_, package_, src), 563 (any) { throw new Exception(format("No override exists for %s %s", package_, version_spec)); }, 564 ); 565 } 566 567 deprecated(OverrideDepMsg) 568 void removeOverride(PlacementLocation scope_, string package_, VersionRange src) 569 { 570 this.removeOverride_(scope_, package_, src); 571 } 572 573 package(dub) void removeOverride_(PlacementLocation scope_, string package_, VersionRange src) 574 { 575 Location* rep = &m_repositories[scope_]; 576 foreach (i, ovr; rep.overrides) { 577 if (ovr.package_ != package_ || ovr.source != src) 578 continue; 579 rep.overrides = rep.overrides[0 .. i] ~ rep.overrides[i+1 .. $]; 580 (*rep).writeOverrides(); 581 return; 582 } 583 throw new Exception(format("No override exists for %s %s", package_, src)); 584 } 585 586 deprecated("Use `store(NativePath source, PlacementLocation dest, string name, Version vers)`") 587 Package storeFetchedPackage(NativePath zip_file_path, Json package_info, NativePath destination) 588 { 589 return this.store_(zip_file_path, destination, package_info["name"].get!string, 590 Version(package_info["version"].get!string)); 591 } 592 593 /** 594 * Store a zip file stored at `src` into a managed location `destination` 595 * 596 * This will extracts the package supplied as (as a zip file) to the 597 * `destination` and sets a version field in the package description. 598 * In the future, we should aim not to alter the package description, 599 * but this is done for backward compatibility. 600 * 601 * Params: 602 * src = The path to the zip file containing the package 603 * dest = At which `PlacementLocation` the package should be stored 604 * name = Name of the package being stored 605 * vers = Version of the package 606 * 607 * Returns: 608 * The `Package` after it has been loaded. 609 * 610 * Throws: 611 * If the package cannot be loaded / the zip is corrupted / the package 612 * already exists, etc... 613 */ 614 Package store(NativePath src, PlacementLocation dest, string name, Version vers) 615 { 616 NativePath dstpath = this.getPackagePath(dest, name, vers.toString()); 617 if (!dstpath.existsFile()) 618 mkdirRecurse(dstpath.toNativeString()); 619 // For libraries leaking their import path 620 dstpath = dstpath ~ name; 621 622 // possibly wait for other dub instance 623 import core.time : seconds; 624 auto lock = lockFile(dstpath.toNativeString() ~ ".lock", 30.seconds); 625 if (dstpath.existsFile()) { 626 this.refresh(false); 627 return this.getPackage(name, vers, dest); 628 } 629 return this.store_(src, dstpath, name, vers); 630 } 631 632 /// Backward-compatibility for deprecated overload, simplify once `storeFetchedPatch` 633 /// is removed 634 private Package store_(NativePath src, NativePath destination, string name, Version vers) 635 { 636 import std.range : walkLength; 637 638 logDebug("Placing package '%s' version '%s' to location '%s' from file '%s'", 639 name, vers, destination.toNativeString(), src.toNativeString()); 640 641 if( existsFile(destination) ){ 642 throw new Exception(format("%s (%s) needs to be removed from '%s' prior placement.", 643 name, vers, destination)); 644 } 645 646 // open zip file 647 ZipArchive archive; 648 { 649 logDebug("Opening file %s", src); 650 auto f = openFile(src, FileMode.read); 651 scope(exit) f.close(); 652 archive = new ZipArchive(f.readAll()); 653 } 654 655 logDebug("Extracting from zip."); 656 657 // In a github zip, the actual contents are in a subfolder 658 alias PSegment = typeof(NativePath.init.head); 659 PSegment[] zip_prefix; 660 outer: foreach(ArchiveMember am; archive.directory) { 661 auto path = NativePath(am.name).bySegment.array; 662 foreach (fil; packageInfoFiles) 663 if (path.length == 2 && path[$-1].name == fil.filename) { 664 zip_prefix = path[0 .. $-1]; 665 break outer; 666 } 667 } 668 669 logDebug("zip root folder: %s", zip_prefix); 670 671 NativePath getCleanedPath(string fileName) { 672 auto path = NativePath(fileName); 673 if (zip_prefix.length && !path.bySegment.startsWith(zip_prefix)) return NativePath.init; 674 static if (is(typeof(path[0 .. 1]))) return path[zip_prefix.length .. $]; 675 else return NativePath(path.bySegment.array[zip_prefix.length .. $]); 676 } 677 678 static void setAttributes(string path, ArchiveMember am) 679 { 680 import std.datetime : DosFileTimeToSysTime; 681 682 auto mtime = DosFileTimeToSysTime(am.time); 683 setTimes(path, mtime, mtime); 684 if (auto attrs = am.fileAttributes) 685 std.file.setAttributes(path, attrs); 686 } 687 688 // extract & place 689 mkdirRecurse(destination.toNativeString()); 690 logDebug("Copying all files..."); 691 int countFiles = 0; 692 foreach(ArchiveMember a; archive.directory) { 693 auto cleanedPath = getCleanedPath(a.name); 694 if(cleanedPath.empty) continue; 695 auto dst_path = destination ~ cleanedPath; 696 697 logDebug("Creating %s", cleanedPath); 698 if( dst_path.endsWithSlash ){ 699 if( !existsDirectory(dst_path) ) 700 mkdirRecurse(dst_path.toNativeString()); 701 } else { 702 if( !existsDirectory(dst_path.parentPath) ) 703 mkdirRecurse(dst_path.parentPath.toNativeString()); 704 // for symlinks on posix systems, use the symlink function to 705 // create them. Windows default unzip doesn't handle symlinks, 706 // so we don't need to worry about it for Windows. 707 version(Posix) { 708 import core.sys.posix.sys.stat; 709 if( S_ISLNK(cast(mode_t)a.fileAttributes) ){ 710 import core.sys.posix.unistd; 711 // need to convert name and target to zero-terminated string 712 auto target = toStringz(cast(const(char)[])archive.expand(a)); 713 auto dstFile = toStringz(dst_path.toNativeString()); 714 enforce(symlink(target, dstFile) == 0, "Error creating symlink: " ~ dst_path.toNativeString()); 715 goto symlink_exit; 716 } 717 } 718 719 { 720 auto dstFile = openFile(dst_path, FileMode.createTrunc); 721 scope(exit) dstFile.close(); 722 dstFile.put(archive.expand(a)); 723 } 724 setAttributes(dst_path.toNativeString(), a); 725 symlink_exit: 726 ++countFiles; 727 } 728 } 729 logDebug("%s file(s) copied.", to!string(countFiles)); 730 731 // overwrite dub.json (this one includes a version field) 732 auto pack = Package.load(destination, NativePath.init, null, vers.toString()); 733 734 if (pack.recipePath.head != defaultPackageFilename) 735 // Storeinfo saved a default file, this could be different to the file from the zip. 736 removeFile(pack.recipePath); 737 pack.storeInfo(); 738 addPackages(this.m_internal.localPackages, pack); 739 return pack; 740 } 741 742 /// Removes the given the package. 743 void remove(in Package pack) 744 { 745 logDebug("Remove %s, version %s, path '%s'", pack.name, pack.version_, pack.path); 746 enforce(!pack.path.empty, "Cannot remove package "~pack.name~" without a path."); 747 748 // remove package from repositories' list 749 bool found = false; 750 bool removeFrom(Package[] packs, in Package pack) { 751 auto packPos = countUntil!("a.path == b.path")(packs, pack); 752 if(packPos != -1) { 753 packs = .remove(packs, packPos); 754 return true; 755 } 756 return false; 757 } 758 foreach(repo; m_repositories) { 759 if (removeFrom(repo.fromPath, pack)) { 760 found = true; 761 break; 762 } 763 // Maintain backward compatibility with pre v1.30.0 behavior, 764 // this is equivalent to remove-local 765 if (removeFrom(repo.localPackages, pack)) { 766 found = true; 767 break; 768 } 769 } 770 if(!found) 771 found = removeFrom(this.m_internal.localPackages, pack); 772 enforce(found, "Cannot remove, package not found: '"~ pack.name ~"', path: " ~ to!string(pack.path)); 773 774 logDebug("About to delete root folder for package '%s'.", pack.path); 775 rmdirRecurse(pack.path.toNativeString()); 776 logInfo("Removed", Color.yellow, "%s %s", pack.name.color(Mode.bold), pack.version_); 777 } 778 779 /// Compatibility overload. Use the version without a `force_remove` argument instead. 780 deprecated("Use `remove(pack)` directly instead, the boolean has no effect") 781 void remove(in Package pack, bool force_remove) 782 { 783 remove(pack); 784 } 785 786 Package addLocalPackage(NativePath path, string verName, PlacementLocation type) 787 { 788 path.endsWithSlash = true; 789 auto pack = Package.load(path); 790 enforce(pack.name.length, "The package has no name, defined in: " ~ path.toString()); 791 if (verName.length) 792 pack.version_ = Version(verName); 793 794 // don't double-add packages 795 Package[]* packs = &m_repositories[type].localPackages; 796 foreach (p; *packs) { 797 if (p.path == path) { 798 enforce(p.version_ == pack.version_, "Adding the same local package twice with differing versions is not allowed."); 799 logInfo("Package is already registered: %s (version: %s)", p.name, p.version_); 800 return p; 801 } 802 } 803 804 addPackages(*packs, pack); 805 806 this.m_repositories[type].writeLocalPackageList(); 807 808 logInfo("Registered package: %s (version: %s)", pack.name, pack.version_); 809 return pack; 810 } 811 812 void removeLocalPackage(NativePath path, PlacementLocation type) 813 { 814 path.endsWithSlash = true; 815 816 Package[]* packs = &m_repositories[type].localPackages; 817 size_t[] to_remove; 818 foreach( i, entry; *packs ) 819 if( entry.path == path ) 820 to_remove ~= i; 821 enforce(to_remove.length > 0, "No "~type.to!string()~" package found at "~path.toNativeString()); 822 823 string[Version] removed; 824 foreach (i; to_remove) 825 removed[(*packs)[i].version_] = (*packs)[i].name; 826 827 *packs = (*packs).enumerate 828 .filter!(en => !to_remove.canFind(en.index)) 829 .map!(en => en.value).array; 830 831 this.m_repositories[type].writeLocalPackageList(); 832 833 foreach(ver, name; removed) 834 logInfo("Deregistered package: %s (version: %s)", name, ver); 835 } 836 837 /// For the given type add another path where packages will be looked up. 838 void addSearchPath(NativePath path, PlacementLocation type) 839 { 840 m_repositories[type].searchPath ~= path; 841 this.m_repositories[type].writeLocalPackageList(); 842 } 843 844 /// Removes a search path from the given type. 845 void removeSearchPath(NativePath path, PlacementLocation type) 846 { 847 m_repositories[type].searchPath = m_repositories[type].searchPath.filter!(p => p != path)().array(); 848 this.m_repositories[type].writeLocalPackageList(); 849 } 850 851 void refresh(bool refresh) 852 { 853 logDiagnostic("Refreshing local packages (refresh existing: %s)...", refresh); 854 855 foreach (ref repository; this.m_repositories) 856 repository.scanLocalPackages(refresh, this); 857 858 this.m_internal.scan(this, refresh); 859 foreach (ref repository; this.m_repositories) 860 repository.scan(this, refresh); 861 862 foreach (ref repository; this.m_repositories) 863 repository.loadOverrides(); 864 } 865 866 alias Hash = ubyte[]; 867 /// Generates a hash digest for a given package. 868 /// Some files or folders are ignored during the generation (like .dub and 869 /// .svn folders) 870 Hash hashPackage(Package pack) 871 { 872 string[] ignored_directories = [".git", ".dub", ".svn"]; 873 // something from .dub_ignore or what? 874 string[] ignored_files = []; 875 SHA256 hash; 876 foreach(file; dirEntries(pack.path.toNativeString(), SpanMode.depth)) { 877 const isDir = file.isDir; 878 if(isDir && ignored_directories.canFind(NativePath(file.name).head.name)) 879 continue; 880 else if(ignored_files.canFind(NativePath(file.name).head.name)) 881 continue; 882 883 hash.put(cast(ubyte[])NativePath(file.name).head.name); 884 if(isDir) { 885 logDebug("Hashed directory name %s", NativePath(file.name).head); 886 } 887 else { 888 hash.put(openFile(NativePath(file.name)).readAll()); 889 logDebug("Hashed file contents from %s", NativePath(file.name).head); 890 } 891 } 892 auto digest = hash.finish(); 893 logDebug("Project hash: %s", digest); 894 return digest[].dup; 895 } 896 897 /// Adds the package and scans for subpackages. 898 private void addPackages(ref Package[] dst_repos, Package pack) 899 const { 900 // Add the main package. 901 dst_repos ~= pack; 902 903 // Additionally to the internally defined subpackages, whose metadata 904 // is loaded with the main dub.json, load all externally defined 905 // packages after the package is available with all the data. 906 foreach (spr; pack.subPackages) { 907 Package sp; 908 909 if (spr.path.length) { 910 auto p = NativePath(spr.path); 911 p.normalize(); 912 enforce(!p.absolute, "Sub package paths must be sub paths of the parent package."); 913 auto path = pack.path ~ p; 914 if (!existsFile(path)) { 915 logError("Package %s declared a sub-package, definition file is missing: %s", pack.name, path.toNativeString()); 916 continue; 917 } 918 sp = Package.load(path, NativePath.init, pack); 919 } else sp = new Package(spr.recipe, pack.path, pack); 920 921 // Add the subpackage. 922 try { 923 dst_repos ~= sp; 924 } catch (Exception e) { 925 logError("Package '%s': Failed to load sub-package %s: %s", pack.name, 926 spr.path.length ? spr.path : spr.recipe.name, e.msg); 927 logDiagnostic("Full error: %s", e.toString().sanitize()); 928 } 929 } 930 } 931 } 932 933 deprecated(OverrideDepMsg) 934 alias PackageOverride = PackageOverride_; 935 936 package(dub) struct PackageOverride_ { 937 private alias ResolvedDep = SumType!(NativePath, Version); 938 string package_; 939 VersionRange source; 940 ResolvedDep target; 941 942 deprecated("Use `source` instead") 943 @property inout(Dependency) version_ () inout return @safe { 944 return Dependency(this.source); 945 } 946 947 deprecated("Assign `source` instead") 948 @property ref PackageOverride version_ (Dependency v) scope return @safe pure { 949 this.source = v.visit!( 950 (VersionRange range) => range, 951 (any) { 952 int a; if (a) return VersionRange.init; // Trick the compiler 953 throw new Exception("Cannot use anything else than a `VersionRange` for overrides"); 954 }, 955 ); 956 return this; 957 } 958 959 deprecated("Use `target.match` directly instead") 960 @property inout(Version) targetVersion () inout return @safe pure nothrow @nogc { 961 return this.target.match!( 962 (Version v) => v, 963 (any) => Version.init, 964 ); 965 } 966 967 deprecated("Assign `target` directly instead") 968 @property ref PackageOverride targetVersion (Version v) scope return pure nothrow @nogc { 969 this.target = v; 970 return this; 971 } 972 973 deprecated("Use `target.match` directly instead") 974 @property inout(NativePath) targetPath () inout return @safe pure nothrow @nogc { 975 return this.target.match!( 976 (NativePath v) => v, 977 (any) => NativePath.init, 978 ); 979 } 980 981 deprecated("Assign `target` directly instead") 982 @property ref PackageOverride targetPath (NativePath v) scope return pure nothrow @nogc { 983 this.target = v; 984 return this; 985 } 986 987 deprecated("Use the overload that accepts a `VersionRange` as 2nd argument") 988 this(string package_, Dependency version_, Version target_version) 989 { 990 this.package_ = package_; 991 this.version_ = version_; 992 this.target = target_version; 993 } 994 995 deprecated("Use the overload that accepts a `VersionRange` as 2nd argument") 996 this(string package_, Dependency version_, NativePath target_path) 997 { 998 this.package_ = package_; 999 this.version_ = version_; 1000 this.target = target_path; 1001 } 1002 1003 this(string package_, VersionRange src, Version target) 1004 { 1005 this.package_ = package_; 1006 this.source = src; 1007 this.target = target; 1008 } 1009 1010 this(string package_, VersionRange src, NativePath target) 1011 { 1012 this.package_ = package_; 1013 this.source = src; 1014 this.target = target; 1015 } 1016 } 1017 1018 deprecated("Use `PlacementLocation` instead") 1019 enum LocalPackageType : PlacementLocation { 1020 package_ = PlacementLocation.local, 1021 user = PlacementLocation.user, 1022 system = PlacementLocation.system, 1023 } 1024 1025 private enum LocalPackagesFilename = "local-packages.json"; 1026 private enum LocalOverridesFilename = "local-overrides.json"; 1027 1028 /** 1029 * A managed location, with packages, configuration, and overrides 1030 * 1031 * There exists three standards locations, listed in `PlacementLocation`. 1032 * The user one is the default, with the system and local one meeting 1033 * different needs. 1034 * 1035 * Each location has a root, under which the following may be found: 1036 * - A `packages/` directory, where packages are stored (see `packagePath`); 1037 * - A `local-packages.json` file, with extra search paths 1038 * and manually added packages (see `dub add-local`); 1039 * - A `local-overrides.json` file, with manually added overrides (`dub add-override`); 1040 * 1041 * Additionally, each location host a config file, 1042 * which is not managed by this module, but by dub itself. 1043 */ 1044 private struct Location { 1045 /// The absolute path to the root of the location 1046 NativePath packagePath; 1047 1048 /// Configured (extra) search paths for this `Location` 1049 NativePath[] searchPath; 1050 1051 /** 1052 * List of manually registered packages at this `Location` 1053 * and stored in `local-packages.json` 1054 */ 1055 Package[] localPackages; 1056 1057 /// List of overrides stored at this `Location` 1058 PackageOverride_[] overrides; 1059 1060 /** 1061 * List of packages stored under `packagePath` and automatically detected 1062 */ 1063 Package[] fromPath; 1064 1065 this(NativePath path) @safe pure nothrow @nogc 1066 { 1067 this.packagePath = path; 1068 } 1069 1070 void loadOverrides() 1071 { 1072 this.overrides = null; 1073 auto ovrfilepath = this.packagePath ~ LocalOverridesFilename; 1074 if (existsFile(ovrfilepath)) { 1075 logWarn("Found local override file: %s", ovrfilepath); 1076 logWarn(OverrideDepMsg); 1077 logWarn("Replace with a path-based dependency in your project or a custom cache path"); 1078 foreach (entry; jsonFromFile(ovrfilepath)) { 1079 PackageOverride_ ovr; 1080 ovr.package_ = entry["name"].get!string; 1081 ovr.source = VersionRange.fromString(entry["version"].get!string); 1082 if (auto pv = "targetVersion" in entry) ovr.target = Version(pv.get!string); 1083 if (auto pv = "targetPath" in entry) ovr.target = NativePath(pv.get!string); 1084 this.overrides ~= ovr; 1085 } 1086 } 1087 } 1088 1089 private void writeOverrides() 1090 { 1091 Json[] newlist; 1092 foreach (ovr; this.overrides) { 1093 auto jovr = Json.emptyObject; 1094 jovr["name"] = ovr.package_; 1095 jovr["version"] = ovr.source.toString(); 1096 ovr.target.match!( 1097 (NativePath path) { jovr["targetPath"] = path.toNativeString(); }, 1098 (Version vers) { jovr["targetVersion"] = vers.toString(); }, 1099 ); 1100 newlist ~= jovr; 1101 } 1102 auto path = this.packagePath; 1103 if (!existsDirectory(path)) mkdirRecurse(path.toNativeString()); 1104 writeJsonFile(path ~ LocalOverridesFilename, Json(newlist)); 1105 } 1106 1107 private void writeLocalPackageList() 1108 { 1109 Json[] newlist; 1110 foreach (p; this.searchPath) { 1111 auto entry = Json.emptyObject; 1112 entry["name"] = "*"; 1113 entry["path"] = p.toNativeString(); 1114 newlist ~= entry; 1115 } 1116 1117 foreach (p; this.localPackages) { 1118 if (p.parentPackage) continue; // do not store sub packages 1119 auto entry = Json.emptyObject; 1120 entry["name"] = p.name; 1121 entry["version"] = p.version_.toString(); 1122 entry["path"] = p.path.toNativeString(); 1123 newlist ~= entry; 1124 } 1125 1126 NativePath path = this.packagePath; 1127 if( !existsDirectory(path) ) mkdirRecurse(path.toNativeString()); 1128 writeJsonFile(path ~ LocalPackagesFilename, Json(newlist)); 1129 } 1130 1131 // load locally defined packages 1132 void scanLocalPackages(bool refresh, PackageManager manager) 1133 { 1134 NativePath list_path = this.packagePath; 1135 Package[] packs; 1136 NativePath[] paths; 1137 try { 1138 auto local_package_file = list_path ~ LocalPackagesFilename; 1139 if (!existsFile(local_package_file)) return; 1140 1141 logDiagnostic("Loading local package map at %s", local_package_file.toNativeString()); 1142 auto packlist = jsonFromFile(local_package_file); 1143 enforce(packlist.type == Json.Type.array, LocalPackagesFilename ~ " must contain an array."); 1144 foreach (pentry; packlist) { 1145 try { 1146 auto name = pentry["name"].get!string; 1147 auto path = NativePath(pentry["path"].get!string); 1148 if (name == "*") { 1149 paths ~= path; 1150 } else { 1151 auto ver = Version(pentry["version"].get!string); 1152 1153 Package pp; 1154 if (!refresh) { 1155 foreach (p; this.localPackages) 1156 if (p.path == path) { 1157 pp = p; 1158 break; 1159 } 1160 } 1161 1162 if (!pp) { 1163 auto infoFile = Package.findPackageFile(path); 1164 if (!infoFile.empty) pp = Package.load(path, infoFile); 1165 else { 1166 logWarn("Locally registered package %s %s was not found. Please run 'dub remove-local \"%s\"'.", 1167 name, ver, path.toNativeString()); 1168 // Store a dummy package 1169 pp = new Package(PackageRecipe(name), path); 1170 } 1171 } 1172 1173 if (pp.name != name) 1174 logWarn("Local package at %s has different name than %s (%s)", path.toNativeString(), name, pp.name); 1175 pp.version_ = ver; 1176 manager.addPackages(packs, pp); 1177 } 1178 } catch (Exception e) { 1179 logWarn("Error adding local package: %s", e.msg); 1180 } 1181 } 1182 } catch (Exception e) { 1183 logDiagnostic("Loading of local package list at %s failed: %s", list_path.toNativeString(), e.msg); 1184 } 1185 this.localPackages = packs; 1186 this.searchPath = paths; 1187 } 1188 1189 /** 1190 * Scan this location 1191 */ 1192 void scan(PackageManager mgr, bool refresh) 1193 { 1194 // If we're asked to refresh, reload the packages from scratch 1195 auto existing = refresh ? null : this.fromPath; 1196 if (this.packagePath !is NativePath.init) { 1197 // For the internal location, we use `fromPath` to store packages 1198 // loaded by the user (e.g. the project and its subpackages), 1199 // so don't clean it. 1200 this.fromPath = null; 1201 } 1202 foreach (path; this.searchPath) 1203 this.scanPackageFolder(path, mgr, existing); 1204 if (this.packagePath !is NativePath.init) 1205 this.scanPackageFolder(this.packagePath, mgr, existing); 1206 } 1207 1208 /** 1209 * Scan the content of a folder (`packagePath` or in `searchPaths`), 1210 * and add all packages that were found to this location. 1211 */ 1212 void scanPackageFolder(NativePath path, PackageManager mgr, 1213 Package[] existing_packages) 1214 { 1215 if (!path.existsDirectory()) 1216 return; 1217 1218 logDebug("iterating dir %s", path.toNativeString()); 1219 try foreach (pdir; iterateDirectory(path)) { 1220 logDebug("iterating dir %s entry %s", path.toNativeString(), pdir.name); 1221 if (!pdir.isDirectory) continue; 1222 1223 // Old / flat directory structure, used in non-standard path 1224 // Packages are stored in $ROOT/$SOMETHING/` 1225 auto pack_path = path ~ (pdir.name ~ "/"); 1226 auto packageFile = Package.findPackageFile(pack_path); 1227 1228 // New (since 2015) managed structure: 1229 // $ROOT/$NAME-$VERSION/$NAME 1230 // This is the most common code path 1231 if (mgr.isManagedPath(path) && packageFile.empty) { 1232 foreach (subdir; iterateDirectory(path ~ (pdir.name ~ "/"))) 1233 if (subdir.isDirectory && pdir.name.startsWith(subdir.name)) { 1234 pack_path ~= subdir.name ~ "/"; 1235 packageFile = Package.findPackageFile(pack_path); 1236 break; 1237 } 1238 } 1239 1240 if (packageFile.empty) continue; 1241 Package p; 1242 try { 1243 foreach (pp; existing_packages) 1244 if (pp.path == pack_path) { 1245 p = pp; 1246 break; 1247 } 1248 if (!p) p = Package.load(pack_path, packageFile); 1249 mgr.addPackages(this.fromPath, p); 1250 } catch (ConfigException exc) { 1251 // Confiy error message already include the path 1252 logError("Invalid recipe for local package: %S", exc); 1253 } catch (Exception e) { 1254 logError("Failed to load package in %s: %s", pack_path, e.msg); 1255 logDiagnostic("Full error: %s", e.toString().sanitize()); 1256 } 1257 } 1258 catch (Exception e) 1259 logDiagnostic("Failed to enumerate %s packages: %s", path.toNativeString(), e.toString()); 1260 } 1261 1262 /** 1263 * Looks up already-loaded packages at a specific version 1264 * 1265 * Looks up a package according to this `Location`'s priority, 1266 * that is, packages from the search path and local packages 1267 * have the highest priority. 1268 * 1269 * Params: 1270 * name = The full name of the package to look up 1271 * ver = The version to look up 1272 * 1273 * Returns: 1274 * A `Package` if one was found, `null` if none exists. 1275 */ 1276 private inout(Package) lookup(string name, Version ver) inout { 1277 foreach (pkg; this.localPackages) 1278 if (pkg.name == name && pkg.version_.matches(ver, VersionMatchMode.strict)) 1279 return pkg; 1280 foreach (pkg; this.fromPath) 1281 if (pkg.name == name && pkg.version_.matches(ver, VersionMatchMode.strict)) 1282 return pkg; 1283 return null; 1284 } 1285 1286 /** 1287 * Get the final destination a specific package needs to be stored in. 1288 * 1289 * Note that there needs to be an extra level for libraries like `ae` 1290 * which expects their containing folder to have an exact name and use 1291 * `importPath "../"`. 1292 * 1293 * Hence the final format should be `$BASE/$NAME-$VERSION/$NAME`, 1294 * but this function returns `$BASE/$NAME-$VERSION/` 1295 * `$BASE` is `this.packagePath`. 1296 */ 1297 private NativePath getPackagePath (string name, string vers) 1298 { 1299 // + has special meaning for Optlink 1300 string clean_vers = vers.chompPrefix("~").replace("+", "_"); 1301 NativePath result = this.packagePath ~ (name ~ "-" ~ clean_vers); 1302 result.endsWithSlash = true; 1303 return result; 1304 } 1305 } 1306 1307 private immutable string OverrideDepMsg = 1308 "Overrides are deprecated as they are redundant with more fine-grained approaches";